之前实现了@SingleInstance注解的编译器处理器,实现添加@SingleInstance就可以把类变成单例,这里记录一下实现让idea也能看懂这个注解的的插件的过程。

Lombok Idea 插件修改

跑一下原Lombok Idea 插件

先去github clone一份代码到本地

用idea打开项目,这是一个gradle项目,项目配置文件为build.gradle,打开这个文件找到repositories修改gradle仓库地址:

1
2
3
4
5
6
7
repositories{
mavenLocal()
maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
}

在右边的gradle面板中点击reimport 一段时间后就下载好了相应的依赖,最大的应该是ideaIC***,也就是idea社区版的依赖 应该有好几百m

然后点开task-> intellij 就可以看到很多gradle任务,运行一下runlde发现可以跑起来了。

阅读Lombok Idea 插件源码

大致看一下java目录下各目录都是干嘛的:

  • action:顶部菜单新插入的action按钮
  • extension:功能扩展
  • processor:注解处理器
  • provider:同样提供功能扩展
  • ps: 扩展的psi元素
  • util: 一些共用的工具类

再看找一个跟@SingleInstance处理类似的注解,看是怎么处理的。这里选择@ToString,因为不关注添加的方法具体细节,两个注解的逻辑是一样的,都是如果有这个注解,就判断类有没有特定名称的方法,如果没有则添加一个。通过该类可以找到提供注解处理的类为:ToStringProcessor,然后搜索该类结果为:searchToString

推测依据ToStringProcessor 再实现一个SingleInstanceProcessor, 然后再搜索到的这些类的对应位置也加上新的处理类就能完成需求了,其中最重要到的显然就是实现这个Processor。

不管整体的逻辑原理是啥,先来看一下ToStringProcessor内部,到底是个什么逻辑,要实现一个新的处理器需要干些啥

先是构造函数:

1
2
3
4
5
public ToStringProcessor(@NotNull EqualsAndHashCodeToStringHandler equalsAndHashCodeToStringHandler) {
super(PsiMethod.class, ToString.class);
handler = equalsAndHashCodeToStringHandler;
}

这个点进去super里面查看父类的构造函数就可以知道,这个是定义这个处理器支持的psi元素类型以及需要处理的的注解类型。

然后是validate和几个validate开头的私有函数:

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
@Override
protected boolean validate(@NotNull PsiAnnotation psiAnnotation, @NotNull PsiClass psiClass, @NotNull ProblemBuilder builder) {
final boolean result = validateAnnotationOnRigthType(psiClass, builder);
if (result) {
validateExistingMethods(psiClass, builder);
}

final Collection<String> excludeProperty = PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "exclude", String.class);
final Collection<String> ofProperty = PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "of", String.class);

if (!excludeProperty.isEmpty() && !ofProperty.isEmpty()) {
builder.addWarning("exclude and of are mutually exclusive; the 'exclude' parameter will be ignored",
PsiQuickFixFactory.createChangeAnnotationParameterFix(psiAnnotation, "exclude", null));
} else {
validateExcludeParam(psiClass, builder, psiAnnotation, excludeProperty);
}
validateOfParam(psiClass, builder, psiAnnotation, ofProperty);

return result;
}

private boolean validateAnnotationOnRigthType(@NotNull PsiClass psiClass, @NotNull ProblemBuilder builder) {
boolean result = true;
if (psiClass.isAnnotationType() || psiClass.isInterface()) {
builder.addError("@ToString is only supported on a class or enum type");
result = false;
}
return result;
}

看一下是哪里调用了这个validate:

1
2
3
4
5
6
7
PsiAnnotation psiAnnotation = PsiAnnotationSearchUtil.findAnnotation(psiClass, getSupportedAnnotationClasses());
if (null != psiAnnotation) {
if (supportAnnotationVariant(psiAnnotation) && validate(psiAnnotation, psiClass, ProblemEmptyBuilder.getInstance())) {
result = new ArrayList<>();
generatePsiElements(psiClass, psiAnnotation, result);
}
}

这样就清晰了,validate是用来判断需不需要处理的,generatePsiElements是实现具体的处理逻辑,就是修改psi元素。

回到ToStringProcessor, 果然马上就是generatePsiElements方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void generatePsiElements(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation, @NotNull List<? super PsiElement> target) {
target.addAll(createToStringMethod(psiClass, psiAnnotation));
}

@NotNull
public PsiMethod createToStringMethod(@NotNull PsiClass psiClass, @NotNull Collection<MemberInfo> memberInfos, @NotNull PsiAnnotation psiAnnotation, boolean forceCallSuper) {
final PsiManager psiManager = psiClass.getManager();

final String paramString = createParamString(psiClass, memberInfos, psiAnnotation, forceCallSuper);
final String blockText = String.format("return \"%s(%s)\";", getSimpleClassName(psiClass), paramString);

final LombokLightMethodBuilder methodBuilder = new LombokLightMethodBuilder(psiManager, METHOD_NAME)
.withMethodReturnType(PsiType.getJavaLangString(psiManager, GlobalSearchScope.allScope(psiClass.getProject())))
.withContainingClass(psiClass)
.withNavigationElement(psiAnnotation)
.withModifier(PsiModifier.PUBLIC);
methodBuilder.withBody(PsiMethodUtil.createCodeBlockFromText(blockText, methodBuilder));
return methodBuilder;
}

这里就是需要创建一个ToString方法。

新增@SingleInstance的处理

先参照ToStringProcessor 实现一个SingleInstance, 有一些明显不需要的就直接删掉:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* @author sictiy.xu
* @version 2019/10/14 15:31
**/
public class SingleInstanceProcessor extends AbstractClassProcessor
{
private static final String METHOD_NAME = "getInstance";

protected SingleInstanceProcessor()
{
super(PsiMethod.class, SingleInstance.class);
}

@Override
protected boolean validate(@NotNull PsiAnnotation psiAnnotation, @NotNull PsiClass psiClass, @NotNull ProblemBuilder builder)
{
if (!validateAnnotationOnRigthType(psiClass, builder))
{
return false;
}
if (!validateExistingMethods(psiClass, builder))
{
return false;
}
return true;
}

private boolean validateAnnotationOnRigthType(@NotNull PsiClass psiClass, @NotNull ProblemBuilder builder)
{
boolean result = true;
if (psiClass.isAnnotationType() || psiClass.isInterface())
{
builder.addError("@SingleInstance is only supported on a class or enum type");
result = false;
}
return result;
}

private boolean validateExistingMethods(@NotNull PsiClass psiClass, @NotNull ProblemBuilder builder)
{
boolean result = true;

final Collection<PsiMethod> classMethods = PsiClassUtil.collectClassMethodsIntern(psiClass);
if (PsiMethodUtil.hasMethodByName(classMethods, METHOD_NAME))
{
builder.addWarning("Not generated '%s'(): A method with same name already exists", METHOD_NAME);
result = false;
}

return result;
}

@Override
protected void generatePsiElements(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation, @NotNull List<? super PsiElement> target)
{
target.addAll(createGetInstanceMethod(psiClass, psiAnnotation));
}

private Collection<PsiMethod> createGetInstanceMethod(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation)
{
final PsiMethod method = createOneGetInstanceMethod(psiClass, psiAnnotation);
return Collections.singletonList(method);
}

private PsiMethod createOneGetInstanceMethod(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation)
{
final PsiManager psiManager = psiClass.getManager();

final LombokLightMethodBuilder methodBuilder = new LombokLightMethodBuilder(psiManager, METHOD_NAME);
methodBuilder.withMethodReturnType(PsiClassUtil.getTypeWithGenerics(psiClass));
methodBuilder.withContainingClass(psiClass);
methodBuilder.withNavigationElement(psiAnnotation);
methodBuilder.withModifier(PsiModifier.PUBLIC);
methodBuilder.withModifier(PsiModifier.STATIC);
return methodBuilder;
}

@Override
public LombokPsiElementUsage checkFieldUsage(@NotNull PsiField psiField, @NotNull PsiAnnotation psiAnnotation)
{
return LombokPsiElementUsage.NONE;
}
}

然后修改ToStringProcessor涉及到的类,在ToStringProcessor用到的类里,有大部分其实并不需要,最后修改到的地方只有两处,第一个是LombokProcessorManager, 还有一个是plugin.xml里面

修改后编译运行,发现已经可以用了。果然在现有基础上改就是这么简单。

简化体验idea插件开发

在原有的基础上修改简单,但也仅仅了解了Processor里面的逻辑。但对Processor的process方法什么时候调用,idea怎么触发找到processor等等都不知道的。就算不深入,简单的了解还是要有的, 不然下次需要重头开始就懵了。

plugin.xml

这个类似于项目文件,插件的名称,作者,更新日志,版本等都在这里配置,同时也相当于实现清单,告诉idea这个插件实现了哪些接口。

配置方面就不说了,直接修改就好了,实现的接口主要分为两部分,actions 和 extensions, 相当于被动技能和主动技能:

  • action extends AnAction 扩展菜单栏

    • 重写 actionPerformed() action被点击调用
    • 重写 update() 点击菜单栏使action菜单项显示出来时,回调update函数,用于根据当前的idea环境判断action是不是可点击
    • 在plugin.xml 中可以通过add-to-group来指定这个action按钮放在哪
  • extensions 高亮,语法支持等扩展

    • 重写 initComponent()
    • 重写 disposeComponent()
    • application idea 启动的时候初始化
    • project 打开每个project的时候初始化
    • module 打开每个module的时候初始化

主要功能扩展接口

  • PsiAugmentProvider

    • 在idea眼里 所有的类,方法,属性都是psi
    • 该接口可以动态扩展psi元素, 修改psi元素让idea眼中的类,方法等变成我们想要的样子,比如增加一个方法, 修改返回值等。
    • 扩展的方法编译时不会生成 即仅编辑器可见
    • 重写 getAugments() 针对每一个传入的psi元素进行扩展,第二个参数为元素的类型
  • StructureViewExtension

    • 扩展Structure功能, 就是经常看到的Structure面板
    • 重写 getChildren() 修改Structure界面看到的子元素 将自己添加的虚拟元素加入返回的结果中即可

开始动手

Action

实现一个最简单的action:

1
2
3
4
5
6
7
8
public class SingleAction extends AnAction
{
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent)
{
LogUtil.info("click single action");
}
}

然后再plugin.xml中声明一下:

1
2
3
<action class="com.sictiy.plugin.action.SingleAction" id="singleAction" text="SingleAction">
<add-to-group group-id="ToolsMenu" anchor="first"/>
</action>

然后就可以看到啦:
action

Processor

先声明我们需要实现的Processor,以及所有Processor的管理组件:

1
2
3
<!-- Add your extensions here -->
<applicationService serviceImplementation="com.sictiy.plugin.processor.impl.SingleInstanceProcessor"/>
<projectService serviceImplementation="com.sictiy.plugin.processor.ProcessorComponent"/>

然后再实现他们,SingleInstanceProcessor和之前一样,就不说了,ProcessorComponent维护两个hashtable, 以Processor支持的psi元素,和支持的注解为key,缓存所有的Processor实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProcessorComponent
{
private final Map<Class<? extends PsiElement>, Collection<Processor>> classProcessorMap;
private final Map<String, Collection<Processor>> annotationProcessorMap;
...

public Collection<Processor> getClassProcessors(@NotNull Class<? extens PsiElement> supportedClass)
...

public Collection<Processor> getAnnotationProcessors(@NotNull PsiAnnotation psiAnnotation)
...

}

Extends

PsiAugmentProvider

实现getAugments方法, 根据传入的psi元素和类型,返回处理后的所有元素,这里使用CachedValueProvider来提供缓存功能,已提高获取时的性能。CachedValueProvider 根据psi元素类型的不同,分为三个不同的静态实例,每个实例内部维护一个RecursionGuard<PsiClass>实现缓存, compute方法通过resursionGuard返回处理后的元素列表。在getPsis中,实际处理传入的元素,遍历所有支持该类型的处理器,依次处理,将结果组合成list返回:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MyPsiAugmentProvider extends PsiAugmentProvider
{
protected <Psi extends PsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type)
{
...
}

private static class MyCachedValueProvider<Psi extends PsiElement> implements CachedValueProvider<List<Psi>>
{
private final Class<Psi> type;
private final PsiClass psiClass;
private final RecursionGuard<PsiClass> recursionGuard;

private MyCachedValueProvider(Class<Psi> type, PsiClass psiClass, String guardName)
{
this.type = type;
this.psiClass = psiClass;
this.recursionGuard = RecursionManager.createGuard(guardName);
}

private MyCachedValueProvider(Class<Psi> type, PsiClass psiClass, RecursionGuard<PsiClass> recursionGuard)
{
this.type = type;
this.psiClass = psiClass;
this.recursionGuard = recursionGuard;
}

@Nullable
@Override
public Result<List<Psi>> compute()
{
return recursionGuard.doPreventingRecursion(psiClass, true, () -> Result.create(getPsis(psiClass, type), psiClass));
}

// 根据传入的psi元素, 由相应的处理器 处理后 将结果返回
private static <Psi extends PsiElement> List<Psi> getPsis(PsiClass psiClass, Class<Psi> type)
{
final List<Psi> result = new ArrayList<>();
final Collection<Processor> processors = ProcessorComponent.getInstance(psiClass.getProject()).getClassProcessors(type);
for (Processor processor : processors)
{
final List<? super PsiElement> elements = processor.process(psiClass);
elements.forEach(element -> result.add((Psi) element));
}
return result;
}
}
}
StructureViewExtension

重写getChildren() 方法 找出传入元素的所有子元素中属于自己额外添加的那一部分,放到一个列表中返回即可:

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
34
35
36
37
public class MyStructureViewExtension implements StructureViewExtension
{
@Override
public Class<? extends PsiElement> getType()
{
return PsiClass.class;
}

/**
* 将自定义元素添加的structure面板
*
* @param psiElement psiElement
* @return com.intellij.ide.structureView.StructureViewTreeElement[]
**/
@Override
public StructureViewTreeElement[] getChildren(PsiElement psiElement)
{
final PsiClass parentClass = (PsiClass) psiElement;
final Stream<PsiMethodTreeElement> methods = Arrays.stream(parentClass.getMethods())
.filter(MyLightMethodBuilder.class::isInstance)
.map(psiMethod -> new PsiMethodTreeElement(psiMethod, false));
final Stream<PsiFieldTreeElement> fields = Arrays.stream(parentClass.getFields())
.filter(MyLightFieldBuilder.class::isInstance)
.map(psiField -> new PsiFieldTreeElement(psiField, false));
final Stream<JavaClassTreeElement> classes = Arrays.stream(parentClass.getInnerClasses())
.filter(MyLightClassBuilder.class::isInstance)
.map(psiClass -> new JavaClassTreeElement(psiClass, false));
return Stream.concat(Stream.concat(fields, methods), classes).toArray(StructureViewTreeElement[]::new);
}

@Nullable
@Override
public Object getCurrentEditorElement(Editor editor, PsiElement psiElement)
{
return null;
}
}

over

  • 项目源码:github
  • 编译运行后的样子:
    result