编写一个IDEA插件之:使用PSI分析Java代码
PSI
是Program Structure Interface
的缩写,即程序结构接口。
如果我们想要分析源代码文件的内容就离不开PSI
。
我们知道,JVM
在加载类之前,首先需要读取Class
文件,并将Class
文件解析成一个结构体对象,对应的是Class
文件结构。与JVM
解析Class
文件不同的是,IDEA
解析的是Java
源代码,但IDEA
也是将Java
文件解析为一个结构体对象。
请记住一句话,对于任何拥有固定结构的文件或者代码,都可以使用访问者模式。
不仅Java
文件,任何代码文件都会有一定的结构,否则编译器也不能识别,也是因为如此,IDEA
实现的PSI
与Java
字节码操作工具ASM
有非常多的相似之处,除了都是将文件解析成结构外,也都支持使用访问者模式编辑文件,一个大的结构下面包含许多小的结构,小的结构也支持使用访问者模式编辑。
因为很相似,所以我们可以用学习使用ASM
工具分析、创建、或改写Class
文件的思维去学习PSI
。
由于不同的编程语言编写的代码文件有不同的结构,IDEA
将文件结构抽象为接口,叫程序结构接口文件(PSI File
),不同类型的文件解析后生成不同的PsiFile
接口的实现类实例,这也是IDEA
能够扩展支持多语言的基础。
PsiFile
接口
一个文件就是一个PsiFile
,也是一个文件的结构树的根节点,PsiFile
是一个接口,如果文件是一个.java
文件,那么解析生成的PsiFile
就是PsiJavaFile
对象,如果是一个Xml
文件,则解析后生成的是XmlFile
对象。
PsiElement
接口
Class
文件结构包含字段表、属性表、方法表等,每个字段、方法也都有属性表,但在PSI
中,总体上只有PsiFile
和PsiElement
。
Element
即元素,一个PsiFile
(本身也是PsiElement
)由许多的PsiElement
构成,每个PsiElement
也可以由许多的PsiElement
构成。
PsiElement
用于描述源代码的内部结构,不同的结构对应不同的实现类。
对应Java
文件的PsiElement
种类有:PsiClass
、PsiField
、PsiMethod
、PsiCodeBlock
、PsiStatement
、PsiMethodCallExpression
等等。其中,PsiField
、PsiMethod
都是PsiClass
的子元素,PsiCodeBlock
是PsiMethod
的子元素,PsiMethodCallExpression
是PsiCodeBlock
的子元素,正是这种关系构造成了一棵树。
解析一个Java
文件有上百种类型的PsiElement
,对于一个新手,我们如何才能快速的认识对应Java
代码文件中的每行代码都会解析生成呢?好在IDEA
提供了PSI
视图查看器。
如果你正在编写插件,那么IDEA
会自动在“工具”菜单中显示“查看PSI
结构”的选项,否则,我们需要修改IDEA
的配置文件才能在“工具”菜单中看到这个选项。
配置文件在IDEA
安装路径的bin
目录下,找到idea.properties
文件,如下图所示。
我们需要在idea.properties
文件中添加这样一行配置:
idea.is.internal=true
添加配置后重启IDEA
就能看到tools
菜单下新加了两个选择,如下图所示。
其中View PSI Structure of Current File
是将当前查看的文件解析为结构树,选中选项后弹出如下图所示的窗口。
-
Show PSI structure for
:选择PsiFile
类型; -
Show PsiWhiteSpace
:去掉勾选后可以隐藏表示连续空格(包括换行符)的元素PsiElement
;
当我们选中源码时,IDEA
会找到对应的PsiElement
标志为选中状态,如上图左侧的PSI Tree
窗口所示。
PsiReference
一个PsiReference
表示代码中某个PsiElement
链接到相应的声明。
简单理解,PsiReference
就是我们选中鼠标右键弹出菜单中Go To
的Declaration or Usages
、或者按住command
键+鼠标点击后能够跳转到相应声明的依据。
我们可以通过调用PsiElement#getReference
方法获取一个PsiElement
的PsiReference
,然后调用PsiReference#resolve
方法取得该PsiElement
链接到(引用)的PsiElement
。
例如,获取一个方法调用表达式PsiMethodCallExpression
链接到声明的PsiElement
可以这样写。
下面是这段代码的一次调试的截图:
如上图所示,此次PsiMethodCallExpression
表示的是payConfigApplicationService.createOrUpdate(dto)
,PsiMethodCallExpression
也是一个PsiElement
,可以调用getReference
获取到该元素的PsiReference
实例,最后调用PsiReference
实例的resolve
方法取得该方法调用表达式元素链接到的声明是一个PsiMethod
,表示createOrUpdate
方法。
我们还可以继续获取该表达式链接到的PsiMethod
所属的类PsiClass
。
通过分析一个元素的PsiReference
,我们可以判断一行代码是否有调用某个类的方法,如果有,则在代码行号处显示一个图标,点击图标跳转到目标方法等。
总之,要想在自定义插件中分析源代码就不得不了解PSI
。
后记
笔者是通过阅读官方文档、通过PSI
查看器学习了解PSI
、并通过分析MybatisX
这个插件的源码,以及自己动手不断试错学习如何编写一个IDEA
插件的,这与笔者以前学习ASM
操作字节码一样,都是瞎折腾,但不畏惧困难。
[Java艺术] 微信号:javaskill
一个只推送原创文章的技术公众号,分享Java后端相关技术。
- Android中基于监听的事件处理
- 新推出的GridLayout网格布局
- Android scrollview嵌套webview滑动冲突的解决方案
- 几乎不用但要了解的AbsoluteLayout绝对布局
- 两分钟掌握FrameLayout帧布局
- 善用TableLayout表格布局,事半功倍
- 轻松掌握RelativeLayout相对布局
- 如何正确使用padding和margin
- 两种对齐方式,layout_gravity和gravity大不同
- GridView属性和使用方法
- ListView数据动态更新
- ListView优化和列表首尾使用
- 自定义BaseAdapter
- 使用SimpleAdapter
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- FFmpeg进行音频的解码和播放
- Gitbook 安装及使用
- MySQL 视图、过程、函数
- 基于Spring Boot + Dubbo的全链路日志追踪(二)
- 基于Spring Boot + Dubbo的全链路日志追踪(一)
- 使用C语言编写Python扩展包
- PlantUML基本使用(一)--时序图
- gRPC基本使用(一)--java与go之间的相互调用
- confd基本使用--Nginx配置自动化
- JVM自定义类加载器
- Java代理相关:JDK动态代理、CGLIB动态代理
- Tomcat CPU占用100%异常分析与处理
- Solr基本搭建及MySQL配置
- Tomcat, Jre 证书相关
- zookeeper集群搭建