• Javac 编译自定义注解及分析 Lombok 的注解实现


    在上一篇中 ,我留下了几个疑问,我们使用lombok的注解时,为什么加了个注解就可以帮我们自动生成代码呢?是谁给我们做了这件事情呢?它的原理是什么样的呢?

    本篇就是以我们最常用的 lombok作为主线来引出 javac 注解处理器,Lombok 插件注解功能很多,出了有自动 set、get 方法外,还有链式调用、建造者模式等等,但是我们就讨论最简单的 set、get 方法的生成。

    一、用Lombok引出问题

    1.1、引入

    1、idea 中打开 settings (快捷键:ctrl+alt+s) ,搜索 plugin ,在 plugins 里面搜索 lombok ,安装

    2、在项目中引入 lombok 的依赖

    1. <dependency>
    2. <groupId>org.projectlombokgroupId>
    3. <artifactId>lombokartifactId>
    4. <version>1.18.24version>
    5. dependency>
    6. 复制代码

    1.2、优缺

    Lombok 是一个 Java 库,能自动插入编辑器并构建工具,简化 Java 开发。通过添加注解的方式,不需要为类编写 getter或 eques 方法,同时可以自动化日志变量。官网链接

    优点:简化 Java 开发,减少了许多重复代码。

    缺点

    • 降低了源码的可读性和完整性;
    • 有可能会破坏封装性,因为有些属性并不需要向外暴露;
    • 降低了可调试性;
    • Lombok 会帮我们自动生成很多代码,但这些代码是在编译期生成的,因此在开发和调试阶段这些代码可能是“丢失的”,这就给调试代码带来了很大的不便。

    如果不考虑的那么严谨,我觉得还是要用的,因为我懒。

    1.3、使用

    写一个类来分析一下:

    我们自己手写的一个JavaBean

    1. /**
    2. * @description:
    3. * @author: Yihui Wang
    4. * @date: 2022年07月06日 20:23
    5. */
    6. public class Student {
    7. private String name;
    8. private String age;
    9. public String getName() {
    10. return name;
    11. }
    12. public void setName(String name) {
    13. this.name = name;
    14. }
    15. public String getAge() {
    16. return age;
    17. }
    18. public void setAge(String age) {
    19. this.age = age;
    20. }
    21. }
    22. 复制代码

    用了 lombok 注解的 JavaBean

    1. /**
    2. * @description:
    3. * @author: Yihui Wang
    4. * @date: 2022年07月06日 20:23
    5. */
    6. @Data
    7. public class StudentLombok {
    8. private String name;
    9. private String age;
    10. }
    11. 复制代码

    我们编译一下,Idea 中点击顶部菜单 Build ,下拉选择 Recompile 看看他们生成的 class文件是什么样的。

    可以明显看出,使用了 @Setter、@Getter 注解后,和我们手动编写的 Java 代码,编译完的结果是一样的。

    它直接帮我们生成了这些方法,这些步骤究竟是谁做的勒?我们是否也可以自己编写这样的注解呢?

    二、Lombok 原理分析

    其实这里面用到了 AOP 编程的编译时织入技术,就是在编译的时候修改最终 class 文件。

    大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前,都会按照如下图所示的各个步骤进行:

    Javac 的编译过程

    归纳起来主要是由以下三个过程组成:

    • 分析和输入到符号表
    • 注解处理
    • 语义分析和生成 class 文件

    而Lombok 正是利用注解处理这一步来进行实现的。Lombok 使用的是 JDK 6 实现的 JSR 269: Pluggable Annotation Processing API (编译期的注解处理器) ,它允许在编译期处理注解,读取、修改、添加抽象语法树中的内容。

    其实说到这里,我们还只是知道它是在这一步处理的,但如何处理的,我们还是一无所知。

    稍后我们会手动实现 Lombok 中的 @Getter、@Setter 注解,这里先事先说明可能会牵扯到的知识。

    • 主要使用到的都是 jdk 源码的 tools.ja 包
    • 使用的 api 主要是com.sun.tools.javac包下的
    • 抽象语法 JCTree 使用

    不懂也没关系,我也不是很懂,哈哈,我也只是因为好奇,才来探寻的

    其中最主要的就是牵扯到的AbstractProcessor抽象注解处理类,还有就是 JCTree 相关的api,这些的话,我也用的不多,不敢胡乱发言。

    要实现注解处理器首先要做的就是继承抽象类 javax.annotation.processing.AbstractProcessor,然后重写它的 process() 方法,process() 方法是 javac 编译器在执行注解处理器代码时要执行的过程。

    1. /**
    2. 一个抽象注释处理器,旨在成为大多数具体注释处理器的方便超类。
    3. */
    4. public abstract class AbstractProcessor implements Processor {
    5. /**
    6. * Processing environment providing by the tool framework.
    7. */
    8. protected ProcessingEnvironment processingEnv;
    9. private boolean initialized = false;
    10. /**
    11. 如果处理器类使用SupportedOptions进行注释,则返回一个不可修改的集合,该集合与注释的字符串集相同。
    12. 如果类没有这样注释,则返回一个空集
    13. */
    14. public Set getSupportedOptions() {
    15. SupportedOptions so = this.getClass().getAnnotation(SupportedOptions.class);
    16. if (so == null)
    17. return Collections.emptySet();
    18. else
    19. return arrayToSet(so.value());
    20. }
    21. /**
    22. 如果处理器类使用SupportedAnnotationTypes进行注释,则返回一个不可修改的集合,
    23. 该集合具有与注释相同的字符串集。如果类没有这样注释,则返回一个空集。
    24. return:
    25. 此处理器支持的注释类型的名称,如果没有则为空集
    26. */
    27. public Set getSupportedAnnotationTypes() {
    28. SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
    29. if (sat == null) {
    30. if (isInitialized())
    31. processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
    32. "No SupportedAnnotationTypes annotation " +
    33. "found on " + this.getClass().getName() +
    34. ", returning an empty set.");
    35. return Collections.emptySet();
    36. }
    37. else
    38. return arrayToSet(sat.value());
    39. }
    40. /**
    41. 如果处理器类使用SupportedSourceVersion进行注解,则在注解中返回源版本。
    42. 如果类没有这样注释,则返回SourceVersion.RELEASE_6
    43. */
    44. public SourceVersion getSupportedSourceVersion() {
    45. SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
    46. SourceVersion sv = null;
    47. if (ssv == null) {
    48. sv = SourceVersion.RELEASE_6;
    49. if (isInitialized())
    50. processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
    51. "No SupportedSourceVersion annotation " +
    52. "found on " + this.getClass().getName() +
    53. ", returning " + sv + ".");
    54. } else
    55. sv = ssv.value();
    56. return sv;
    57. }
    58. /**
    59. 该方法有两个参数,“annotations” 表示此处理器所要处理的注解集合;
    60. “roundEnv” 表示当前这个 Round 中的语法树节点,
    61. 每个语法树节点都表示一个 Element(javax.lang.model.element.ElementKind 可以查看到相关 Element)。
    62. */
    63. public abstract boolean process(Set annotations,
    64. RoundEnvironment roundEnv);
    65. }
    66. 复制代码

    另外还有这两个用来配合的 注解:

    1. @SupportedAnnotationTypes("*")
    2. @SupportedSourceVersion(SourceVersion.RELEASE_8)
    3. 复制代码

    @SupportedAnnotationTypes 表示注解处理器对哪些注解感兴趣,“*” 表示对所有的注解都感兴趣;@SupportedSourceVersion 指出这个注解处理器可以处理最高哪个版本的 Java 代码。

    三、简易版 Lombok 实现

    简要说明

    先说说我们要实现的东西,为了简单的去理解,我这里只讨论get、set 方法,其实里面的实现都差不多,如果偏要说不同的话,就是调用的javac的api不同吧。

    写了两个注解 :@MyGetter@MySetter 和他们的处理器 MyAnnotationProcessor

    注解处理器,顾名思义就是用来处理注解的啦。

    项目结构:

    由于是maven项目,这里面引用了com.sun.tools的东西,所以,需要在maven的pom文件里面加上,这样,在使用maven打包的时候,才不会报错。

    1. <dependency>
    2. <groupId>com.sungroupId>
    3. <artifactId>toolsartifactId>
    4. <version>1.8version>
    5. <scope>systemscope>
    6. <systemPath>jdk路径/lib/tools.jarsystemPath>
    7. dependency>
    8. 复制代码

    我们这里利用 Java SPI 加载自定义注解器的方式,生成一个 jar 包,类似于 Lombok ,这样之后其它应用一旦引用了这个 jar 包,自定义注解器就能自动生效了。

    SPI是java提供的一种服务发现的标准,具体请看SPI介绍,但每次我们都需要自己创建services目录,以及配置文件,google的autoservice就可以帮我们省去这一步。

    1. <dependency>
    2. <groupId>com.google.auto.servicegroupId>
    3. <artifactId>auto-serviceartifactId>
    4. <version>1.0-rc5version>
    5. dependency>
    6. 复制代码

    如果你使用Processor(javax.annotation.processing.Processor),并且你的元数据文件被包含在了一个jar包中,同时这个jar包是在javac(java编译)的classpath路径下时,javac会自动的执行通过该方式注入进去的Processor的实现类,以实现对于该项目内的相关数据的扩展。

    使用 AutoService 会自动的生成META-INF./services/javax.annotation.processing.Processor 文件,并且文件中内容就是我们动态注入进去的类。

    然后在编译的时候就会执行对应的扩展方法,同时写入文件。

    项目代码

    代码都很简单,所以除了注解处理器,其他的都没有带啥注释啦哈。

    1. @Target({ElementType.TYPE})
    2. @Retention(RetentionPolicy.SOURCE)
    3. public @interface MyGetter {
    4. }
    5. 复制代码
    1. @Target({ElementType.TYPE})
    2. @Retention(RetentionPolicy.SOURCE)
    3. public @interface MySetter {
    4. }
    5. 复制代码
    1. package com.nzc.my_annotation;
    2. import com.google.auto.service.AutoService;
    3. import com.sun.source.tree.Tree;
    4. import com.sun.tools.javac.api.JavacTrees;
    5. import com.sun.tools.javac.code.Flags;
    6. import com.sun.tools.javac.code.Type;
    7. import com.sun.tools.javac.processing.JavacProcessingEnvironment;
    8. import com.sun.tools.javac.tree.JCTree;
    9. import com.sun.tools.javac.tree.TreeMaker;
    10. import com.sun.tools.javac.tree.TreeTranslator;
    11. import com.sun.tools.javac.util.*;
    12. import javax.annotation.processing.*;
    13. import javax.lang.model.SourceVersion;
    14. import javax.lang.model.element.Element;
    15. import javax.lang.model.element.TypeElement;
    16. import java.util.Set;
    17. /**
    18. * @author nzc
    19. */
    20. @SupportedAnnotationTypes({"com.nzc.my_annotation.MyGetter","com.nzc.my_annotation.MySetter"})
    21. @SupportedSourceVersion(SourceVersion.RELEASE_8)
    22. @AutoService(Processor.class)
    23. public class MyAnnotationProcessor extends AbstractProcessor {
    24. private JavacTrees javacTrees; // 提供了待处理的抽象语法树
    25. private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
    26. private Names names; // 提供了创建标识符的方法
    27. /**
    28. * 从Context中初始化JavacTrees,TreeMaker,Names
    29. */
    30. @Override
    31. public synchronized void init(ProcessingEnvironment processingEnv) {
    32. super.init(processingEnv);
    33. Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
    34. javacTrees = JavacTrees.instance(processingEnv);
    35. treeMaker = TreeMaker.instance(context);
    36. names = Names.instance(context);
    37. }
    38. @Override
    39. public boolean process(Set annotations, RoundEnvironment roundEnv) {
    40. // 返回使用给定注释类型注释的元素的集合。
    41. Setextends Element> get = roundEnv.getElementsAnnotatedWith(MyGetter.class);
    42. for (Element element : get) {
    43. // 获取当前类的抽象语法树
    44. JCTree tree = javacTrees.getTree(element);
    45. // 获取抽象语法树的所有节点
    46. // Visitor 抽象内部类,内部定义了访问各种语法节点的方法
    47. tree.accept(new TreeTranslator() {
    48. @Override
    49. public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
    50. // 在抽象树中找出所有的变量
    51. // 过滤,只处理变量类型
    52. jcClassDecl.defs.stream()
    53. .filter(it -> it.getKind().equals(Tree.Kind.VARIABLE))
    54. // 类型强转
    55. .map(it -> (JCTree.JCVariableDecl) it)
    56. .forEach(it -> {
    57. // 对于变量进行生成方法的操作
    58. jcClassDecl.defs = jcClassDecl.defs.prepend(genGetterMethod(it));
    59. });
    60. super.visitClassDef(jcClassDecl);
    61. }
    62. });
    63. }
    64. Setextends Element> set = roundEnv.getElementsAnnotatedWith(MySetter.class);
    65. for (Element element : set) {
    66. JCTree tree = javacTrees.getTree(element);
    67. tree.accept(new TreeTranslator() {
    68. @Override
    69. public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
    70. jcClassDecl.defs.stream()
    71. .filter(it -> it.getKind().equals(Tree.Kind.VARIABLE))
    72. .map(it -> (JCTree.JCVariableDecl) it)
    73. .forEach(it -> {
    74. jcClassDecl.defs = jcClassDecl.defs.prepend(genSetterMethod(it));
    75. });
    76. super.visitClassDef(jcClassDecl);
    77. }
    78. });
    79. }
    80. return true;
    81. }
    82. private JCTree.JCMethodDecl genGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
    83. // 生成return语句,return this.xxx
    84. JCTree.JCReturn returnStatement = treeMaker.Return(
    85. treeMaker.Select(
    86. treeMaker.Ident(names.fromString("this")),
    87. jcVariableDecl.getName()
    88. )
    89. );
    90. ListBuffer statements = new ListBuffer().append(returnStatement);
    91. // public 方法访问级别修饰
    92. JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
    93. // 方法名 getXXX ,根据字段名生成首字母大写的get方法
    94. Name getMethodName = createGetMethodName(jcVariableDecl.getName());
    95. // 返回值类型,get类型的返回值类型与字段类型一致
    96. JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;
    97. // 生成方法体
    98. JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
    99. // 泛型参数列表
    100. List methodGenericParamList = List.nil();
    101. // 参数值列表
    102. List parameterList = List.nil();
    103. // 异常抛出列表
    104. List throwCauseList = List.nil();
    105. // 生成方法定义树节点
    106. return treeMaker.MethodDef(
    107. // 方法访问级别修饰符
    108. modifiers,
    109. // get 方法名
    110. getMethodName,
    111. // 返回值类型
    112. returnMethodType,
    113. // 泛型参数列表
    114. methodGenericParamList,
    115. //参数值列表
    116. parameterList,
    117. // 异常抛出列表
    118. throwCauseList,
    119. // 方法默认体
    120. body,
    121. // 默认值
    122. null
    123. );
    124. }
    125. private JCTree.JCMethodDecl genSetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
    126. // this.xxx=xxx
    127. JCTree.JCExpressionStatement statement = treeMaker.Exec(
    128. treeMaker.Assign(
    129. treeMaker.Select(
    130. treeMaker.Ident(names.fromString("this")),
    131. jcVariableDecl.getName()
    132. ),
    133. treeMaker.Ident(jcVariableDecl.getName())
    134. )
    135. );
    136. ListBuffer statements = new ListBuffer().append(statement);
    137. // set方法参数
    138. JCTree.JCVariableDecl param = treeMaker.VarDef(
    139. // 访问修饰符
    140. treeMaker.Modifiers(Flags.PARAMETER, List.nil()),
    141. // 变量名
    142. jcVariableDecl.name,
    143. //变量类型
    144. jcVariableDecl.vartype,
    145. // 变量初始值
    146. null
    147. );
    148. // 方法访问修饰符 public
    149. JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
    150. // 方法名(setXxx),根据字段名生成首选字母大写的set方法
    151. Name setMethodName = createSetMethodName(jcVariableDecl.getName());
    152. // 返回值类型void
    153. JCTree.JCExpression returnMethodType = treeMaker.Type(new Type.JCVoidType());
    154. // 生成方法体
    155. JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
    156. // 泛型参数列表
    157. List methodGenericParamList = List.nil();
    158. // 参数值列表
    159. List parameterList = List.of(param);
    160. // 异常抛出列表
    161. List throwCauseList = List.nil();
    162. // 生成方法定义语法树节点
    163. return treeMaker.MethodDef(
    164. // 方法级别访问修饰符
    165. modifiers,
    166. // set 方法名
    167. setMethodName,
    168. // 返回值类型
    169. returnMethodType,
    170. // 泛型参数列表
    171. methodGenericParamList,
    172. // 参数值列表
    173. parameterList,
    174. // 异常抛出列表
    175. throwCauseList,
    176. // 方法体
    177. body,
    178. // 默认值
    179. null
    180. );
    181. }
    182. private Name createGetMethodName(Name variableName) {
    183. String fieldName = variableName.toString();
    184. return names.fromString("get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
    185. }
    186. private Name createSetMethodName(Name variableName) {
    187. String fieldName = variableName.toString();
    188. return names.fromString("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
    189. }
    190. }
    191. 复制代码
    1. @MyGetter
    2. @MySetter
    3. public class School {
    4. private String name;
    5. private String address;
    6. }
    7. 复制代码

    测试结果:

    编译

    编译的话,直接编译根项目就好,我原本想着先编译子项目,再编译另外一个项目,但是会报错,不想纠结了,放出来的,是经过测试的,可以正常编译出来的。

    四、思考

    相信大家都有使用Lombok的过程,但是不知道大家有没有注意到我上面的demo是存在一些问题的呢?

    我们之前分析了 lombok 它是在编译时为我们添加了诸如 set、get 方法的,但实际上我们在开发的时候就已经可以调用对象上的 set、get 方法啦,这又是如何实现的呢?

    我个人的测试及思考

    这里一定是会牵扯到 idea 中的 lombok 插件的问题,如果你只是引入了 lombok 的依赖,没有安装 lombok 插件的话,那么在 idea 是不会有方法提示的。

    我新建了一个空白项目进行测试,如果你引入依赖,没安装插件,idea 只会爆红,但是是可以通过编译的,也不会报错。写一个main 方法也是可以运行的,只不过安装了插件才会提供提示。

    按照这个思路,我又返回去测试了上面的那个 Demo,答案是失败的。

    测试如下:

    1. 创建了 Demo 类,里面写了 main 方法
    2. 创建了 School 对象,调用了 set、get 方法
    3. 使用 maven 编译,成功过,也失败过(问我也是白问,我都测麻了)
    4. 这个可能跟我的机器环境有关,理论上应该是可以成功的,后来新建的一个项目又是可以的。大家也可以去玩一玩。
    5. 启动 main 方法,是直接报错,起不来,原因不知道,如果我还有时间,我再去找找。
    1. java: java.lang.ClassCastException: class com.sun.proxy.$Proxy26 cannot be cast to class com.sun.tools.javac.processing.JavacProcessingEnvironment (com.sun.proxy.$Proxy26 is in unnamed module of loader java.net.URLClassLoader @4fccd51b; com.sun.tools.javac.processing.JavacProcessingEnvironment is in module jdk.compiler of loader 'app')
    2. 复制代码

    lombok 的插件在其中肯定是做了一些事情的,但是我在各大搜索引擎上搜索这方面的知识,也没有找到相关的一些资料,倒是看到有几个小伙伴问出了和我相似的问题。

    如下:大家使用过 lombok 的 @Slf4j 注解吧,为什么有了这个注解,我们就可以直接在类里面使用 log 对象,这个对象又是在哪里创建出来的呢?

    说到这,其实我还是没说出什么道理,因为我也不明白,所以最后这一小节,我的命名才是直接明了的为思考。

    如果有明白的大佬,请不啬赐教,非常感谢!

    此外的补充:

    其实自定义注解处理器,给我的感觉就像 SpringMVC 中拦截器一样,SpringMVC是拦截请求,自定义注解是拦截在编译前,而且给我的感觉的话,自定义注解编译器应该更好玩,并竟可以改 class 文件,感觉之后还有空的话,会继续整一整这个注解处理器。

  • 相关阅读:
    【博客485】prometheus-----为什么prometheus与alertmanager之间要用full mesh,不能load balance
    浅谈一下Vue3的TreeShaking特性
    在一个使用了 Sass 的 React Webpack 项目中安装和使用 Tailwind CSS
    Tlsr8258开发-添加软件定时器
    数据治理-元数据度量指标
    laravel项目通过中间件推送接口调用信息到TransferStatistics项目
    python使用pysqlcipher3对sqlite数据库进行加密
    Vue3 el-tooltip 根据内容控制宽度大小换行和并且内容太短不显示
    Mysql 约束,基本查询,复合查询与函数
    LIEF:修改安卓.so后报 dlopen failed:has invalid shdr offset/size
  • 原文地址:https://blog.csdn.net/weixin_62710048/article/details/126059590