暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

编写一个IDEA插件之:使用PSI分析Java代码

Java艺术 2021-09-07
2664
关注 “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后端相关技术。


文章转载自Java艺术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论