
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
操作字节码一样,都是瞎折腾,但不畏惧困难。
