编译期注解开发指北

发布时间 2023-12-23 16:56:37作者: Ba11ooner

前言

可用于基于注解的工具类开发,主要用于代码生成及相关配套技术

明星项目:Lombok

示例项目:diy-lombok

开发流程

  1. 明确开发目标:代码生成只是一种中间手段,最终必然落到某个具体需求上,非必要不生成
  2. 自定义注解开发
  3. 自定义注解器开发
  4. Debug
    • 基于日志
  5. 作为 SDK 集成到 Spring 项目
    • SDK 端
      • 必要依赖引入 By systemPath
      • META-INF 配置
        • 手动
        • 自动
    • Spring 端
      • 必要依赖引入
      • SDK 引入

项目结构

diy-lombok
  • src

    • main

      • java

        • com.diy.lombok

          • annotation

            • MyGetter.java
            • MySetter.java
          • example:测试用,封装成 SDK 之前可以删除

            • Entity

              package com.diy.lombok.example;
              
              import com.diy.lombok.annotation.MyGetter;
              import com.diy.lombok.annotation.MySetter;
              
              @MySetter
              @MyGetter
              public class Entity {
                  public int id;
                  public String name;
              }
              
          • processor

            • MyGetterProcessor.java
            • MySetterProcessor.java
      • resources

test
  • src
    • main
      • java
        • com.diy.lombok
          • entity
            • Entity.java
          • Application.java
      • resources

开发指南

自定义注解开发

与常规的自定义注解开发无异,注解仍然起到标识和附带额外信息的作用,注解相关语法不变,只是通常会将 RetentionPolicy 设置为 SOURCE,让注解只在编译期存在,过了编译期不再保留

注意区分注解的 Retention 和 生成代码的 Retention,二者没有必然的联系,注解的 Retention 可以手动指定为 SOURCE、CLASS、RUNTIME 三选一。生成代码的 Retention 往往是编译期之后(编译期生成),到运行时一直存在

注解示例
package com.diy.lombok.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MyGetter {
}
package com.diy.lombok.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MySetter {
}

自定义注解处理器开发

  1. 配置支持的版本 By @SupportedSourceVersion
  2. 配置支持的注解 By @SupportedAnnotationTypes
  3. 继承 AbstractProcessor 类,按需求实现 process 方法
注解处理器示例
MyGetterProcessor
package com.diy.lombok.processor;

import com.diy.lombok.annotation.MyGetter;
import com.sun.source.tree.Tree;
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 javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.lang.reflect.Method;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.diy.lombok.annotation.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {

    private Messager messager; // 用于输出编译器日志
    private JavacTrees javacTrees; // 提供了待操作的抽象语法树
    private TreeMaker treeMaker; // 封装了操作 AST 的方法
    private Names names; // 提供了创建标识符的方法

    //region 初始化逻辑:正常初始化 + 获取工具类实例
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        processingEnv = jbUnwrap(ProcessingEnvironment.class, processingEnv);

        this.messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
        messager.printMessage(Diagnostic.Kind.NOTE, "MyGetterProcessor init");
    }
    //endregion

    //region 注解处理逻辑:收集变量,为变量生成 getter 方法
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "MyGetterProcessor process");
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MyGetter.class);
        elements.stream().map(e ->
                javacTrees.getTree(e)).forEach(
                tree -> tree.accept(new TreeTranslator() {
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl tree) {
                        // 创建一个空列表
                        List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                        // 收集所有变量
                        for (JCTree jcTree : tree.defs) {
                            if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                                JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                                jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                            }
                        }

                        // 为所有变量进行方法生成的操作
                        jcVariableDeclList.forEach(jcVariableDecl -> {
                            messager.printMessage(Diagnostic.Kind.NOTE,
                                    getNewMethodName(jcVariableDecl.getName())
                                            + " is created");
                            tree.defs = tree.defs.prepend(makeGetterMethod(jcVariableDecl));
                        });

                        // 执行父类的访问者方法
                        super.visitClassDef(tree);
                    }
                }));
        return false;
    }
    //endregion

    //region 私有方法:用于生成 getter 方法,针对每一个变量
    // getter 方法示例
    // String getName(){
    //     return this.name;
    // }
    private JCTree makeGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
        // 语句列表
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();

        // 创建表达式:this.name
        JCTree.JCExpression variable = treeMaker.Select(
                treeMaker.Ident(names.fromString("this")),
                jcVariableDecl.getName() //获取变量名称
        );

        // 生成 Return 语句:return this.name;
        JCTree.JCReturn returnStatement = treeMaker.Return(variable);

        // 收集语句
        statements.append(returnStatement);

        // 生成代码块
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 生成返回值类型
        JCTree.JCExpression methodType = jcVariableDecl.vartype;

        // 生成返回对象
        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC), // 访问权限标识符
                getNewMethodName(jcVariableDecl.getName()), // 方法名称
                methodType, // 返回值类型
                List.nil(), // 方法的异常列表,这里为空
                List.nil(), // 方法参数列表,这里为空
                List.nil(), // 方法的类型参数列表(方法或类的参数化类型,比如在定义泛型方法或类时,可以指定一些类型参数),这里为空
                block, // 方法体的代码块
                null // 方法参数的默认值(当调用方法时,如果不传入需要的参数,那么这些参数会使用默认值),这里为空
        );
    }
    //endregion

    //region 私有方法:用于生成方法名 getXxx(小驼峰)
    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString(
                "get" + s.substring(0, 1).toUpperCase()
                        + s.substring(1, name.length()));
    }
    //endregion

    //region 解包
    private static <T> T jbUnwrap(Class<? extends T> iface, T wrapper) {
        T unwrapped = null;
        try {
            final Class<?> apiWrappers = wrapper.getClass().getClassLoader().loadClass("org.jetbrains.jps.javac.APIWrappers");
            final Method unwrapMethod = apiWrappers.getDeclaredMethod("unwrap", Class.class, Object.class);
            unwrapped = iface.cast(unwrapMethod.invoke(null, iface, wrapper));
        } catch (Throwable ignored) {
        }
        return unwrapped != null ? unwrapped : wrapper;
    }
    //endregion
}
MySetterProcessor
package com.diy.lombok.processor;

import com.diy.lombok.annotation.MySetter;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
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 javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.lang.reflect.Method;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.diy.lombok.annotation.MySetter")
public class MySetterProcessor extends AbstractProcessor {

    private Messager messager; // 用于输出编译器日志
    private JavacTrees javacTrees; // 提供了待操作的抽象语法树
    private TreeMaker treeMaker; // 封装了操作 AST 的方法
    private Names names; // 提供了创建标识符的方法

    //region 初始化逻辑:正常初始化 + 获取工具类实例
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        // 调用父类方法,正常进行初始化
        super.init(processingEnv);

        processingEnv = jbUnwrap(ProcessingEnvironment.class, processingEnv);

        // 从环境中获取工具类
        this.messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
        messager.printMessage(Diagnostic.Kind.NOTE, "MySetterProcessor init");
    }
    //endregion

    //region 注解处理逻辑:收集变量,为变量生成 setter 方法
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "MySetterProcessor process");
        // 获取带有 @MySetter 注解的类的所有元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MySetter.class);

        // 遍历所有元素
        elements.forEach(e -> {
            //获取元素的 JCTree
            JCTree tree = javacTrees.getTree(e);

            //为 JCTree 创建一个转换器,实现转换器的访问者方法
            tree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl tree) {
                    // 创建一个空列表
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                    // 收集所有变量
                    for (JCTree jcTree : tree.defs) {
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }

                    // 为所有变量进行方法生成的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE,
                                getNewMethodName(jcVariableDecl.getName())
                                        + " is created");
                        tree.defs = tree.defs.prepend(makeSetterMethod(jcVariableDecl));
                    });

                    // 执行父类的访问者方法
                    super.visitClassDef(tree);
                }
            });
        });
        return true;
    }
    //endregion

    //region 私有方法:用于生成 setter 方法,针对每一个变量
    // setter 方法示例
    // void setName(String name){
    //     this.name = name;
    // }
    private JCTree.JCMethodDecl makeSetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
        // 语句列表
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();

        // 生成表达式 this.name = name;
        JCTree.JCExpressionStatement aThis = makeAssignment(
                // 左表达式:this.name
                treeMaker.Select(
                        treeMaker.Ident(names.fromString("this")),
                        jcVariableDecl.getName() //获取变量名称
                ),
                // 右表达式:name
                treeMaker.Ident(jcVariableDecl.getName()));

        // 收集语句
        statements.append(aThis);

        // 生成代码块
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 生成参数
        JCTree.JCVariableDecl param = treeMaker.VarDef(
                treeMaker.Modifiers(Flags.PARAMETER),
                jcVariableDecl.getName(),
                jcVariableDecl.vartype,
                null
        );
        List<JCTree.JCVariableDecl> parameters = List.of(param);

        // 生成返回值类型
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());

        // 生成返回对象:setter 方法
        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC), // 访问权限标识符
                getNewMethodName(jcVariableDecl.getName()), // 方法名称
                methodType, // 返回值类型
                List.nil(), // 方法的异常列表,这里为空
                parameters, // 方法参数列表
                List.nil(), // 方法的类型参数列表(方法或类的参数化类型,比如在定义泛型方法或类时,可以指定一些类型参数),这里为空
                block, // 方法体的代码块
                null // 方法参数的默认值(当调用方法时,如果不传入需要的参数,那么这些参数会使用默认值),这里为空
        );
    }
    //endregion

    //region 私有方法:用于生成方法名 setXxx(小驼峰)
    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString(
                "set" + s.substring(0, 1).toUpperCase()
                        + s.substring(1, name.length()));
    }
    //endregion

    //region 私有方法:用于生成赋值语句
    private JCTree.JCExpressionStatement makeAssignment(
            JCTree.JCExpression leftExpr, JCTree.JCExpression rightExpr) {
        // 返回赋值语句:leftExpr = rightExpr
        return treeMaker.Exec(treeMaker.Assign(leftExpr, rightExpr));
    }
    //endregion

    //region 解包
    private static <T> T jbUnwrap(Class<? extends T> iface, T wrapper) {
        T unwrapped = null;
        try {
            final Class<?> apiWrappers = wrapper.getClass().getClassLoader().loadClass("org.jetbrains.jps.javac.APIWrappers");
            final Method unwrapMethod = apiWrappers.getDeclaredMethod("unwrap", Class.class, Object.class);
            unwrapped = iface.cast(unwrapMethod.invoke(null, iface, wrapper));
        } catch (Throwable ignored) {
        }
        return unwrapped != null ? unwrapped : wrapper;
    }
    //endregion

}

Debug

基于日志

在使用 javac 命令行编译测试时,可以通过 Messager 类进行日志输出

//参数:类别,日志信息
messager.printMessage(Diagnostic.Kind.NOTE, "MySetterProcessor process");
  • Diagnostic.Kind:诊断类型
    • ERROR:错误
    • WARNING:警告
    • MANDATORY_WARNING:强制警告
    • NOTE:标注
    • OTHER:其他
cd src/main/java/com/diy/lombok
# 编译注解 和 注解处理器, 其中 $JAVA_8_HOME 为值为 JDK HOME 目录的环境变量
javac -cp \
$JAVA_8_HOME/lib/tools.jar \
annotation/MyGetter.java \
processor/MyGetterProcessor.java \
annotation/MySetter.java \
processor/MySetterProcessor.java \
-d .
# 引入注解处理器的情况下,编译 Entity,多个注解处理器之间用 , 隔开,不能加入空格
javac -processor com.diy.lombok.processor.MySetterProcessor,com.diy.lombok.processor.MyGetterProcessor example/Entity.java
# 反编译 Entity,查看注解处理器处理后的结果
javap -p example/Entity.class
Compiled from "Entity.java"
public class com.diy.lombok.example.Entity {
  public int id;
  public java.lang.String name;
  public java.lang.String getName();
  public int getId();
  public void setName(java.lang.String);
  public void setId(int);
  public com.diy.lombok.example.Entity();
}

作为 SDK 集成到 Spring 项目

SDK 端

即此处的 diy-lombok,后续将作为 SDK 使用

必要依赖引入

在 Maven 项目中,如果你的代码中使用了 sun 包中的类或方法,那么在执行 mvn install 时就会出现 sun 包不存在的错误。这是因为 sun 包是 Java 的内部包,不建议直接在代码中使用。

然而,IDE(如 IntelliJ IDEA、Eclipse 等)通常会默认引入了 JDK 的内部库,因此在编写代码的时候不会报错。但是当你使用 Maven 进行构建时,Maven 会检查依赖并进行严格的校验,因此可能会出现 sun 包不存在的错误提示。

为了解决这个问题,建议尽量避免直接使用 sun 包中的类或方法,而是使用 JDK 或其他第三方库中提供的替代方案。同时,还可以考虑更新代码以便在不依赖 sun 包的情况下实现相同的功能。

如果一定要用,我们可以通过 systemPath 手动引入 sun 包

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>
        ${java.home}/../lib/tools.jar
    </systemPath>
</dependency>
META-INF 配置
  • 手动

    1. 在 resources 文件夹下创建 META-INF 文件夹

    2. 在 META-INF 文件夹下创建 services 文件夹

    3. 在 services 文件夹中创建 javax.annotation.processing.Processor 文件

    4. javax.annotation.processing.Processor 文件中指定注解处理器的全限定类名

      com.diy.lombok.processor.MyGetterProcessor
      com.diy.lombok.processor.MySetterProcessor
      
  • 自动

    • 依赖导入

      <dependency>
          <groupId>com.google.auto.service</groupId>
          <artifactId>auto-service</artifactId>
          <version>1.0-rc4</version>
      </dependency>
      
    • 注解补充:在注解处理器上增加 @AutoService(Processor.class)

      @SupportedSourceVersion(SourceVersion.RELEASE_8)
      @SupportedAnnotationTypes("com.diy.lombok.annotation.MyGetter")
      @AutoService(Processor.class)
      public class MyGetterProcessor extends AbstractProcessor {
        ...
      }
      
maven install

将 diy-lombok 项目 package 并 install 为本地依赖

Spring 端

使用了 SDK 的 Spring 项目,此处的 test

必要依赖引入
  • spring-boot-starter-web 依赖:与注解处理器无关,Spring Web 项目必备依赖
  • spring-boot-configuration-processor:自动配置
  • diy-lombok:自定义注解及注解处理器
<dependencies>
  
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>diy-lombok</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>2.3.12.RELEASE</version>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>

</dependencies>

参考文档

参考文档:Lombok经常用,但是你知道它的原理是什么吗?

参考文档:Lombok经常用,但是你知道它的原理是什么吗?(二)

参考文档:Java 中的屠龙之术 —— 如何修改语法树

项目源码

只包含 SDK 部分

项目源码:diy-lombok