如何在 IntelliJ IDEA 插件中动态的生成psi代码

在公司的主要工作内容是维护公司内部框架的开发工具,就是 Eclipse 和 IDEA 平台的插件,这两个平台的在国内互联网几乎找不到文章,真是非常小众,今天聊一下我在工作中遇到的一个问题。

需求

我们内部其实有一个类似lombok的包,用于给公司内部的框架对应的一些数据生成相应的代码,比如在类上注解@Field({“ddd_ddd”})就会生成对应的字段和getter/setter。和lombok类似,这个也是在编译时的Annotation Processing生成代码。

关于lombok的原理可以查看

https://www.cnblogs.com/vipstone/p/12597756.html

https://blog.mythsman.com/post/5d2c11c767f841464434a3bf/

一些问题

如果使用记事本进行开发,这种使用方式呢是不会有问题的。但是由于现代的ide都非常智能,能够在开发编码时进行错误校验,那么这种在编译时才生成的代码在ide里面就会显示报错,并且不会有代码提示,使用起来非常不便,所以一般情况下要使用lombok就还需要使用对应ide的插件来解决这个问题。

lombok源码阅读

在Jetbrain官方的开发文档中并没有讲这一部分的内容,而网上这一块的资料也比较少,所以只能去看一下lombok插件的源码。

Intellij平台插件都需要一个声明文件plugin.xml,声明插件的内容。https://plugins.jetbrains.com/docs/intellij/welcome.html?from=jetbrains.org

当然现在版本的lombok-idea插件已经非常复杂了,我们可以把版本回退到早期的commit,这时候plugin.xml里还没有多少东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<idea-plugin version="2">
<name>Lombook Plugin</name>
<description>A plugin that adds first-class support for Project Lombok</description>
<version>0.1</version>
<vendor>Michail Plushnikov</vendor>
<idea-version since-build="8000"/>

<application-components>
<!-- Add your application components here -->
<component>
<implementation-class>de.plushnikov.intellij.plugin.LombokLoader</implementation-class>
</component>
</application-components>

<project-components>
<!-- Add your project components here -->
</project-components>

<actions>
<!-- Add your actions here -->
</actions>

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<lang.psiAugmentProvider implementation="de.plushnikov.intellij.plugin.provider.LombokAugmentProvider"/>
<implicitUsageProvider implementation="de.plushnikov.intellij.plugin.provider.LombokImplicitUsageProvider"/>
<renameHandler implementation="de.plushnikov.intellij.plugin.handler.LombokElementRenameHandler" order="first"/>
<treeGenerator implementation="de.plushnikov.intellij.lombok.psi.MyLightMethodTreeGenerator"/>
<!--<refactoring.changeSignatureUsageProcessor implementation=""/>-->
<!--<refactoring.safeDeleteProcessor -->
<!--<refactoring.moveHandler implementation="de.plushnikov.intellij.plugin.handler.LombokElementMoveHandler" order="first"/>-->
</extensions>
</idea-plugin>

里面重要的参数就是这个 lang.psiAugmentProvider ,但是离谱的是官方文档里面对这个完全没有任何介绍。经过谷歌之后找到了一点介绍 https://intellij-support.jetbrains.com/hc/en-us/community/posts/206127669-Documentation-or-explanation-of-PsiAugmentProvider-https://blog.csdn.net/ExcellentYuXiao/article/details/80273347 那么查过资料之后就能很容易的知道这是干什么的了,这个东西是 IDEA 动态获取每个文件的 psi,添加相应的方法和字段, psi 是 IDEA 内部解析文件和语义转换的一层抽象模型

The Program Structure Interface, commonly referred to as just PSI, is the layer in the IntelliJ Platform responsible for parsing files and creating the syntactic and semantic code model that powers so many of the platform’s features.

对应到Java程序就是PsiJavaFile,PsiClass,PsiMethod,PsiField对应java文件,java类,方法,字段。

生成一个Getter试试

PsiAugmentProvider 继承之后需要重写 getAugments 方法,需要传入3个参数(旧版本2个)PsiElement element, Class type, nameHint。其中element是当前需要扩展psi的 psi 模型。type 是当前所需要的 psi 类型可以通过 type.isAssignableFrom(Class cls) 来判断当前需要的到底是什么 psi 模型。返回值是一个Psi类型的List,这个 list 只是新增的 psi,而 IDEA 自带的该有还是有。

我们需要给标记为 Getter 的字段生成 psi 的 getter 方法,那么我们需要判断一下当前需要的是否是方法

1
boolean isMethod = type.isAssignableFrom(PsiMethod.class); 

如果是方法, 然后可以通过 psiclass 可以获取里面的字段

1
psiClass.getOwnFields();

或者复杂点

1
Arrays.stream(psiClass.getChildren()).filter(PsiField::isInstance).map(PsiField.class::cast).collect(Collectors.toList());

遍历每一个字段,通过PsiField的getAnnotations方法获取他的注解,通过getQualifiedName()可以获得注解的完整包名,这样就能判断每个字段上是否有对应的注解。

1
2
3
4
5
6
7
8
PsiAnnotation[] annotations = psiField.getAnnotations();
for (PsiAnnotation annotation : annotations) {
String annoName = annotation.getQualifiedName();
// 必须填完整包名
if ("pkg1.MyAnnotation".equals(annoName)) {
result.add((Psi) createGetterMethod(psiField));
}
}

生成对应的Psi代码的话可以使用 Idea 提供的类 LightMethodBuilder 。不过可以自己继承一下便于后面生成Structure时使用

1
2
3
4
5
6
7
8
9
10
11
12
String fieldName = psiField.getName();
String methodName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String getterName = "get" + methodName;
LightMethodBuilder methodBuilder = new LightMethodBuilder(psiField.getManager(), JavaLanguage.INSTANCE, getterName);
methodBuilder.addModifier(PsiModifier.PUBLIC);
methodBuilder.setMethodReturnType(psiField.getType());
methodBuilder.setNavigationElement(psiField);
methodBuilder.setContainingClass(psiField.getContainingClass());
boolean isStatic = psiField.hasModifierProperty(PsiModifier.STATIC);
if (isStatic) {
methodBuilder.addModifier(PsiModifier.STATIC);
}

然后对应的getter方法就有了

getter

但是只是做到这一步我们并不能在Structure的界面中看到生成的方法

1
2
3
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.java</depends>

我们需要再实现一个lang.structureViewExtension扩展点。

实现getChildren,需要将传入的类中自己生成的psi模型转化成PsiTreeElement

这里直接复制的lombok的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public StructureViewTreeElement[] getChildren(PsiElement parent) {
final PsiClass parentClass = (PsiClass) parent;

final Stream<PsiFieldTreeElement> lombokFields = Arrays.stream(parentClass.getFields())
.filter(LombokLightFieldBuilder.class::isInstance)
.map(psiField -> new PsiFieldTreeElement(psiField, false));

final Stream<PsiMethodTreeElement> lombokMethods = Arrays.stream(parentClass.getMethods())
.filter(LombokLightMethodBuilder.class::isInstance)
.map(psiMethod -> new PsiMethodTreeElement(psiMethod, false));

final Stream<JavaClassTreeElement> lombokInnerClasses = Arrays.stream(parentClass.getInnerClasses())
.filter(LombokLightClassBuilder.class::isInstance)
.map(psiClass -> new JavaClassTreeElement(psiClass, false));

return Stream.concat(Stream.concat(lombokFields, lombokMethods), lombokInnerClasses)
.toArray(StructureViewTreeElement[]::new);
}

这样在Structure界面中就能看见自己生成的方法了

structureview.png

优化

由于没有加上缓存,所以这里只要在编辑框中随便按键,左侧的Strucure中就会刷新一下,其他地方虽然没有刷新但是感觉对性能也产生了影响。

那么,加个缓存吧

Psi中有个方法,putUserData。传入Key和Value,这里的Key是通过Key.create创建的,然后在Value放入生成的PsiMethod就行了。最后在生成代码之前检查一下是否已经创建过。

1
2
3
4
5
6
7
8
9
10
//存放一个静态变量,不然每次Key.create创建出来的都是新的变量
static final Key<PsiMethod> GETTER_KEY = Key.create("MyGetter");

// 添加缓存
psiClass.putUserData(GETTER_KEY),psiMethod);

// 判断
if (psiField.getUserData(GETTER_KEY) != null) {
return psiField.getUserData(GETTER_KEY);
}

小小的总结

ide的插件开发是真的小众,国内互联网上的资料真是非常少,很多东西都要去找有类似功能的已有插件看源码,希望这篇文章能给不幸和我一样踏上写插件这条路的人一些帮助。