首页 > 其他分享 >编译期注解开发指北

编译期注解开发指北

时间:2023-12-23 17:14:41浏览次数:40  
标签:指北 java 编译 diy import 注解 lombok com JCTree

前言

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

明星项目: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

标签:指北,java,编译,diy,import,注解,lombok,com,JCTree
From: https://www.cnblogs.com/ba11ooner/p/17923308.html

相关文章

  • Jackson Annotations(注解)详解
    转载自:https://blog.csdn.net/wjw465150/article/details/1273268491.概述在本教程中,我们将深入研究JacksonAnnotations。我们将了解如何使用现有的注解,如何创建自定义注解,最后,如何禁用它们。2.Jackson序列化注解首先,我们将看一下序列化注解。2.1.@JsonAnyGetter@J......
  • FOG Project的 FOS 编译
    FOGProject系统是一个免费的开源计算机网络克隆和管理解决方案系统,与传统的Ghost有很大的不同,如果您是计算机维护管理人员,当有大量机器需要同时部署上线的时候FOGProject是一个可以大大提高工作效率的系统,支持windows、Linux等操作系统,包括引导方式也支持UEFI方式进行引导。FO......
  • Spring Boot之@Autowired注解使用区别,实战演示?
    ......
  • Spring基于注解的AOP事务控制
    Spring基于注解的AOP事务控制源码代码测试pom.xml<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schem......
  • K2 sherpa编译使用
    编译安装pip卸载cmake、torch、k2安装cmake3.22.3版本、k2、kaldi_feat【官方提供|install_dir】、torch==2.0.1【】缺cudaexportLD_LIBRARY_PATH=/usr/local/cuda11.7/lib64:$LD_LIBRARY_PATHexportPATH=/usr/local/cuda11.7/include:/usr/local/cuda11.7/bin:$PATH#......
  • Spring 基于注解的AOP面向切面编程
    Spring基于注解的AOP面向切面编程源码代码实现pom.xml<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:sc......
  • 《TVM编译器原理与实践》新书推荐
    《TVM编译器原理与实践》新书推荐作者:吴建明,吴一昊;出版社:机械工业出版社;出版时间:2023年12月 本书已经出版,目前在淘宝天猫,京东,当当上可以购买。谢谢!天猫:https://detail.tmall.com/item.htm?abbucket=8&id=757068341348&ns=1&spm=a21n57.1.0.0.2b9b523ckBk0aH京东:https://it......
  • spring项目中自定义注解
    使用BeanPostProcessorBeanPostProcessor是Spring框架提供的一个接口,用于在Spring容器中对Bean进行后处理。自定义注解后,可以实现一个BeanPostProcessor实现类,在BeanPostProcessor的postProcessAfterInitialization()方法中,使用ClassPathScanningCandidateResol......
  • Flutter子工程编译,Ruby升级及Cocoapods安装问题集
    背景:工程为iOS原生与Flutter混合开发的工程,在编译Flutter子工程的过程中报了一个错,一度让我以为是ruby与pod的版本不兼容导致了一些奇奇怪怪的问题,随即来回折腾了Ruby环境升级与pod的升级安装。问题1:flutter子工程执行了flutterpubget,执行flutterbuildios--no-codesign时,报......
  • 编译并行,link串行
    在CMake中,如果你有多个可执行文件目标,并且你想要它们在链接时串行构建,你可以使用CMake的add_dependencies命令来创建一个依赖链。这将确保在开始构建一个目标之前,它所依赖的目标已经构建完成。下面是一个简化的步骤说明,展示了如何设置CMakeLists.txt来实现多个可执行文件的串行链......