java 插入式注解

本文将通过几个方面来介绍插入式注解。包括如何搭建项目环境,如何编写注解处理器,以及编写 idea 插件使注解生成的代码不会报错。

示例代码地址

什么是插入式注解

JSR-269 插件化注解处理(Pluggable Annotation Processing API)

可以在编译期期间对代码进行处理,像经常用的 lombok 也是基于这个原理,通过注解生成 getter/setter 等方法。

javac 会通过 SPI 机制,找到要调用的注解处理器,处理完源代码后再去生成字节码。

搭建项目环境

在编写注解处理器时,需要通过修改语法树来达成修改代码的效果,java 提供了对应的方法,但限制了访问,得自己引入。

在 jdk8 中,可以通过引入 java安装路径/lib/tools.jar 来实现,而在 jdk8 以上的版本,已经不提供这个 tools.jar 了。那么在 jdk8 以上如何实现效果?就得自定义编译选项了,可以修改 maven 中编译插件的配置来达成目的。

1
2
3
4
5
6
7
8
<compilerArgs>            
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>  
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>  
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>  
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>  
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>  
	<arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>  
</compilerArgs> 

--add-exports--add-opens (lombok 用的是这个)有细微差别,我测试了下,使用 --add-opens 会编译报错,因为我不想纠结这个事情,于是便不求甚解了。

除此之外,还得配置一个参数:-proc:none 。这个是控制 javac 编译时的行为,-proc:none 指不使用注释处理器,只编译源文件。

maven 插件完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<plugin>  
    <groupId>org.apache.maven.plugins</groupId>  
    <artifactId>maven-compiler-plugin</artifactId>  
    <version>3.8.1</version>  
    <configuration>        
	    <source>${maven.compiler.source}</source>  
        <target>${maven.compiler.target}</target>  
        <compilerArgs>         
	        <arg>-proc:none</arg>   
	        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>  
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>  
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>  
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>  
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>  
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>  
        </compilerArgs>    
	</configuration>
</plugin>

这里在顺手放一段 lombok 的配置:lombok/docker/maven/files/jdk-21/classpath/pom.xml

而至于 jdk8 ,事情就简单多了,直接把 tool.jar 引入就完事了。完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
<dependency>  
    <groupId>com.sun</groupId>  
    <artifactId>tools</artifactId>  
    <version>1.8</version>  
    <scope>system</scope>  
    <systemPath>${java.home}/../lib/tools.jar</systemPath>  
</dependency>
...
<plugin>  
    <groupId>org.apache.maven.plugins</groupId>  
    <artifactId>maven-compiler-plugin</artifactId>  
    <version>3.8.1</version>  
    <configuration>        
	    <source>${maven.compiler.source}</source>  
        <target>${maven.compiler.target}</target>  
        <compilerArgs>         
	        <arg>-proc:none</arg>
        </compilerArgs>    
	</configuration>
</plugin>
...

注解处理器编写

接下来进行代码的编写,既然是插入式注解,首先我们需要定义一个注解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.w2tbp.samples.annotation;  
  
import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
  
@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.SOURCE)  
public @interface MyLog {  
}

然后编写注解处理器,得继承 AbstractProcessor 类,并重写 process 方法。在其中我们就能够调用 java 修改语法树的一系列 api,对代码进行一顿输出。

上一节费劲心思配置编译选项,把 tools.jar 引入,就是为了在编写注解处理器的时候调用这些 api。但关于修改语法树,也就是关于 JCTree 的一些语法,怎么使用,我就不多说了,因为我也不太了解 pwq,只是拿来用。

我写的这个注解处理器,是为了让代码生成一段这样的代码:

private final static Logger logger = LogManager.getLogger(Test.class);

完整代码如下:

 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
85
86
87
package com.w2tbp.samples.annotation;  
  
import com.sun.tools.javac.api.JavacTrees;  
import com.sun.tools.javac.code.Flags;  
import com.sun.tools.javac.processing.JavacProcessingEnvironment;  
import com.sun.tools.javac.tree.JCTree;  
import com.sun.tools.javac.tree.TreeMaker;  
import com.sun.tools.javac.tree.TreeTranslator;  
import com.sun.tools.javac.util.*;  
import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;  
  
import javax.annotation.processing.*;  
import javax.lang.model.SourceVersion;  
import javax.lang.model.element.Element;  
import javax.lang.model.element.TypeElement;  
import java.util.Set;  
  
@SupportedAnnotationTypes("com.w2tbp.samples.annotation.MyLog")  
public class MyLogProcessor extends AbstractProcessor {  
  
    private JavacTrees javacTrees;  
    private TreeMaker treeMaker;  
    private Names names;  
  
    private static final Class<MyLog> ANNOTATION_TO_PROCESS = MyLog.class;  
    private static final String LOG_CLASS_TYPE_NAME = Logger.class.getName();  
    private static final String LOG_FACTORY_CLASS_TYPE_NAME = LogManager.class.getName();  
  
    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  
        this.javacTrees = JavacTrees.instance(processingEnv);  
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();  
        this.treeMaker = TreeMaker.instance(context);  
        this.names = Names.instance(context);  
    }  
  
    @Override  
    public SourceVersion getSupportedSourceVersion() {  
        return SourceVersion.latest();  
    }  
  
    @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
        JCTree.JCExpression varTypeName = strToJCExpression(LOG_CLASS_TYPE_NAME);  
        JCTree.JCExpression varFactoryName = strToJCExpression(LOG_FACTORY_CLASS_TYPE_NAME);  
        Name varName = names.fromString("log");  
  
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ANNOTATION_TO_PROCESS);  
        for (Element element : elements) {  
            String className = element.getSimpleName().toString()+".class";  
  
            JCTree tree = javacTrees.getTree(element);  
            tree.accept(new TreeTranslator() {  
                @Override  
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {  
                    JCTree.JCVariableDecl variableDecl = treeMaker.VarDef(  
                            treeMaker.Modifiers(Flags.PRIVATE + Flags.STATIC + Flags.FINAL),  
                            varName, varTypeName,  
                            treeMaker.Apply(  
                                    List.nil(),  
                                    treeMaker.Select(  
                                            varFactoryName,  
                                            names.fromString("getLogger")  
                                    ),  
                                    List.of(strToJCExpression(className))  
                            )  
                    );  
                    jcClassDecl.defs = jcClassDecl.defs.prepend(variableDecl);  
                    super.visitClassDef(jcClassDecl);  
                }  
            });  
        }  
        return true;  
    }  
  
    private JCTree.JCExpression strToJCExpression(String components) {  
        String[] componentArray = components.split("\\.");  
        JCTree.JCExpression expr = treeMaker.Ident(names.fromString(componentArray[0]));  
        for (int i = 1; i < componentArray.length; i++) {  
            expr = treeMaker.Select(expr, names.fromString(componentArray[i]));  
        }  
        return expr;  
    }  
  
}

这样基本的事情就完事了,还剩下一件事,就是这个插入式注解得通过 SPI 的方式被编译器调用,所以需要配置一下。

具体是在 src/main/resources/META-INF/services 目录下新建一个 javax.annotation.processing.Processor 文件

SPI配置文件

内容为注解处理器的路径:

1
com.w2tbp.samples.annotation.MyLogProcessor

除了这种方式,还可以用谷歌的 auto-service 依赖,然后在注解处理器上直接用注解 @AutoService 就行了。

注解处理就告一段落了,完整代码地址在:samples/pluggable-annotation

测试

接下来测试一下。

新建一个项目,引入上面项目的依赖,简单写个测试类,编译后发现,我们想要的结果已经出现了:

.class文件

测试项目代码地址在:samples/pluggable-annotation-test

idea 插件

现在我们知道可以生成语句,那么在代码中就可以用这个生成的对象了。但我们敲下 log 对象时,却出现了问题:

测试类

这个对象飘红了,这是正常的,因为现在并没有这个对象。但是在用 lombok 的时候显然没有这个问题,这是为什么呢?因为 lombok 在 idea 中用了插件,一些较旧的 idea 版本会让人自己装,但现在都集成了。

于是我们也来开发个自己的插件,当我们安装插件后,会看到和 lombok 一样的效果,log 也不报错了,也有代码提示了:

安装插件后

关于如何写插件,新建工程,怎么配置,如何实现,说起来也有点冗杂。代码基本上就是从 lombok 插件中 copy 的,ps:看它的那个插件源代码真是一件痛苦的事情。

插件的代码地址在:samples/pluggable-annotation-idea-plugin

可以直接下载项目目录下的 pluggable-annotation-idea-plugin-1.0-SNAPSHOT.zip ,然后在 idea 插件选项中,选择从磁盘中安装,安装此插件。

另需注意,IDEA 会有版本兼容问题,我这里使用的 IDEA 版本为 2024.1

其他

报错 java: java.lang.IllegalArgumentException

路径:Build, Execution, Deployment -> complier -> Shared build process VM options

加上该配置 -Djps.track.ap.dependencies=false

虚拟机选项

comments powered by Disqus