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

时间:2022-07-28
本文章向大家介绍编写一个IDEA插件之:使用PSI分析Java代码,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

PSIProgram Structure Interface的缩写,即程序结构接口。

如果我们想要分析源代码文件的内容就离不开PSI

我们知道,JVM在加载类之前,首先需要读取Class文件,并将Class文件解析成一个结构体对象,对应的是Class文件结构。与JVM解析Class文件不同的是,IDEA解析的是Java源代码,但IDEA也是将Java文件解析为一个结构体对象。

请记住一句话,对于任何拥有固定结构的文件或者代码,都可以使用访问者模式。

不仅Java文件,任何代码文件都会有一定的结构,否则编译器也不能识别,也是因为如此,IDEA实现的PSIJava字节码操作工具ASM有非常多的相似之处,除了都是将文件解析成结构外,也都支持使用访问者模式编辑文件,一个大的结构下面包含许多小的结构,小的结构也支持使用访问者模式编辑。

因为很相似,所以我们可以用学习使用ASM工具分析、创建、或改写Class文件的思维去学习PSI

由于不同的编程语言编写的代码文件有不同的结构,IDEA将文件结构抽象为接口,叫程序结构接口文件(PSI File),不同类型的文件解析后生成不同的PsiFile接口的实现类实例,这也是IDEA能够扩展支持多语言的基础。

PsiFile接口

一个文件就是一个PsiFile,也是一个文件的结构树的根节点,PsiFile是一个接口,如果文件是一个.java文件,那么解析生成的PsiFile就是PsiJavaFile对象,如果是一个Xml文件,则解析后生成的是XmlFile对象。

PsiElement接口

Class文件结构包含字段表、属性表、方法表等,每个字段、方法也都有属性表,但在PSI中,总体上只有PsiFilePsiElement

Element即元素,一个PsiFile(本身也是PsiElement)由许多的PsiElement构成,每个PsiElement也可以由许多的PsiElement构成。

PsiElement用于描述源代码的内部结构,不同的结构对应不同的实现类。

对应Java文件的PsiElement种类有:PsiClassPsiFieldPsiMethodPsiCodeBlockPsiStatementPsiMethodCallExpression等等。其中,PsiFieldPsiMethod都是PsiClass的子元素,PsiCodeBlockPsiMethod的子元素,PsiMethodCallExpressionPsiCodeBlock的子元素,正是这种关系构造成了一棵树。

解析一个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 ToDeclaration or Usages、或者按住command键+鼠标点击后能够跳转到相应声明的依据。

我们可以通过调用PsiElement#getReference方法获取一个PsiElementPsiReference,然后调用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后端相关技术。