cover_image

Lombok技术揭秘 _ 自动生成带代码的幕后机制

欧文 政采云技术
2023年09月26日 01:00

👆  这是第 374 篇不掺水的原创,想要了解更多,请戳下方卡片关注我们吧~


图片

1. Lombok简介

1.1 Lombok是什么

引入官方解释:

  1. Project Lombok 是一个 JAVA 库,它可以自动插入编辑器和构建工具,为您的 JAVA 锦上添花。

  2. 再也不要写另一个 getter/setter 或 equals 等方法,只要有一个注注解,你的类就有一个功能齐全的生成器,自动记录变量,等等。

1.2 Lombck相关注解功能介绍

注解功能
@Getter
@Setter
1.能够作用在字段或类上生成 get/set 方法;
2.value 设置方法访问级别默认 public ;
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
1.生成无参/全参/( final @Nonnull 参数)的构造方法作用于类。
2. staticName 有值时,会私有当前构造方法,提供指定名称 public 静态构造方法。
3. access 构造器的访问权限,默认public 。
@ToString1.生成类的 toString 方法,作用在类上。
2.属性 includeFieldNames 是否打印字段名称。
3.属性 exclude 排除不希望生成在 toString 中的字段。
4.属性 of 指定生成在 toString 的字段。
5.属性 callSuper 是否生成父类属性在toString。
@Data等同于生成 @ToString、@EqualsAndHashCode、@Getter、@Setter、 @RequiredArgsConstrutor 注解。
@Builder用在类、构造器、方法上提供建造者模式的构建器类,用于创建对象。
@Slf4j在类中生成一个日志记录器( Logger )的字段。
@SneakyThrows方法上自动添加异常处理代码,以使编译器不会抛出异常未处理的警告。
@Value用于创建不可变的值对象( Value Object )类.即所有属性都是final的,并且只有 getter 方法,没有 setter 方法。
@Accessor为属性生成自定义的访问方法,可以控制方法的名称、修饰符、参数等。
@Cleanup为需要关闭的资源自动调用 close 方法,避免资源泄漏。

2. Lombok原理介绍

2.1 Java类文件编译过程

首先,我们知道 Lombok 功能是作用在类编译时期,那我们来看下一个类编译的过程。

  1. 定义一个 PersonDTO.Java 类
public class PersonDTO {
  //姓名
  private String name;  
}
  1. Javac PersonDTO.Java 对源代码进行解析转化,会生成一棵抽象语法树( AST );

  2. 运行过程中会调用实现了 JSR 269 注解处理器,下面介绍;

  3. JSR 实现可处理自定义逻辑,包括可修改编译后的抽象语法树(AST);

  4. Javac 使用修改后的抽象语法树(AST)生成字节码文件;

过程如下图:

图片

AST 是抽象语法树(Abstract Syntax Tree) 的缩写,是 JAVA 源代码展示的一种树状结构它将代码的结构和语法元素映射到树节点上,使得程序可以在编译、

分析和转换过程中更容易地操作和理解。有兴趣可以学习 JavaParser 源码, 了解将 Java 源代码解析生成成一个抽象语法树( AST ),这个树形结构表示了代码的

语法结构包括类、方法、变量、语句等等过程。

github地址:https://github.com/javaparser/javaparser.

如: PersonDTO.Java 在 idea 中使用可视化工具展示文件 AST 树

图片

2.2 JSR 269介绍

首先 JSR 269全称" Pluggable Annotation Processing API ",是 JAVA 平台的一项规范,也被称之为注解处理器 API 。在Java6引入,用于在编译时处理

注解,目标是提供更丰富的编译时元数据处理能力,以增强Java编译器的功能。这个规范允许开发人员创建自定义的注解处理器,这些处理器可以在编译时检查、

分析和生成Java代码。

应用框架:

  1. Servlet、JAX-RS(RESTful Web服务)JSR 269 来生成用于处理 HTTP 请求的代码。

  2. Spring Boot 项目中以处理各种自定义注解,如 @Controller、@Service、@Repository 等。这些注解可以用于自动化配置、依赖注入等方面。

  3. Hibernate 它使用 JSR 269 来处理 JPA 注解,并生成与数据库交互的代码。

  4. Lombok 是一个 JAVA 库,它通过注解处理器生成常见的 JAVA 代码,如 getter、setter、equals、hashCode 等,以简化开发工作。

  5. MapStruct 是一个用于对象映射的 JAVA 库,它使用 JSR 269 来生成类型安全的映射代码,帮助开发人员将一个对象映射到另一个对象。

如何实现自定义注解注解处理器:

1.声明自定义注解;如 Lombok 下的 @Data,@Getter,@Setter等。

2.实现 Process接口,或者继承 AbstractProcessor 复写 process 方法,处理自定义注解逻辑。

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("com.example.MyAnnotation"// 自定义注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) //支持的Java版本
public class MyAnnotationProcessor extends AbstractProcessor {
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)  {
    // 在这里处理自定义注解,生成代码或执行其他任务
    return true;
  }
}

3.注册注解处理器

两种方式:

  1. Resource 文件:项目 META-INF/services 创建 javax.annotation.processing.Processor 文件,自定义注解处理器的全类名写到此文件中。

  2. 通过谷歌工具包 auto-service ,可自动生成以上配置文件。

<!-- 编译注解执行注册 jar包-->
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0.1</version>
</dependency> 

@AutoService(Processor.class) //谷歌工具包方式:注册注解处理器
@SupportedAnnotationTypes("com.example.MyAnnotation") // 自定义注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) //支持的Java版本
public class MyAnnotationProcessor extends AbstractProcessor 
{

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
 
{
    // 在这里处理自定义注解,生成代码或执行其他任务
    return true;
  }
}

2.3 Lombok 实现原理

1. Lombok 实际就是结合注解处理器和 AST 技术, Lombok 实现的注解处理器会遍历 AST ,查找与 Lombok 注解相关的元素,根据注解的要求生成新的代码。

图片

2.编译前后的 AST 语法树对比

图片

加入@Getter注解编译后

图片

3. Lombok 注解处理器,采用 Resource 方式注册编译注解处理器

图片

注解处理器 AnnotationProcessor 源码:

class AnnotationProcessorHider {

 public static class AstModificationNotifierData {
   public volatile static boolean lombokInvoked = false;
 }
 
 public static class AnnotationProcessor extends AbstractProcessor {
  // 获取支持的注解类型
  @Override
  public Set<String> getSupportedOptions() {
   return instance.getSupportedOptions();
  }

  // 获取支持的注解类型
  @Override
  public Set<String> getSupportedAnnotationTypes() {
   return instance.getSupportedAnnotationTypes();
  }

  // 支持的JDK版本
  @Override
  public SourceVersion getSupportedSourceVersion() {
   return instance.getSupportedSourceVersion();
  }

  //初始化环境
  @Override
  public void init(ProcessingEnvironment processingEnv) {
   disableJava9SillyWarning();
   AstModificationNotifierData.lombokInvoked = true;
   instance.init(processingEnv);
   super.init(processingEnv);
  }

  // 处理自定义注解逻辑
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment 
   roundEnv)
 
{
   return instance.process(annotations, roundEnv);
  }
 }

自定义注解处理器 Handler : 在 Jar 包的 lombok.javac.handlers下,每个注解处理对应一个 Handler. 如 HadlerGetter.java 操作 AST 树生成 getter 方法.

图片

2.4手动实现一个 @Getter 功能

2.4.1.创建 maven 工程 demo 包含两个子模块 getter/getter-use图片

2.4.2. getter 工程

pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>getter</artifactId>

  <dependencies>
    <!-- 注解执行器依赖jar -->
    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.6.0</version>
      <scope>system</scope>
      <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>

    <!-- 编译注解执行注册 -->
    <dependency>
      <groupId>com.google.auto.service</groupId>
      <artifactId>auto-service</artifactId>
      <version>1.0.1</version>
    </dependency>
  </dependencies>
</project>

自定义 GetterTest 注解

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

/**
 * @author zcy1
 */

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

GetterTest 编译注解处理器 GetterProcessor

import com.google.auto.service.AutoService;
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.util.Set;

/**
 * @author zcy1
 */

@AutoService(Processor.class) //谷歌工具包方式:注册注解处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("GetterTest")
public class GetterProcessor extends AbstractProcessor 
{

  /**
   * 用于在编译器打印消息的组件
   */

  private Messager messager;
  
  /**
   * 提供待处理抽象语法树
   */

  private JavacTrees trees;
  
  /**
   * 用来构造语法树节点
   */

  private TreeMaker treeMaker;
  
  /**
   * 用于创建标识符的对象
   */

  private Names names;
  
  /**
   * 获取一些注解处理器执行处理逻辑时一些关键对象
   * @param processingEnv 处理环境
   */

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    this.messager = processingEnv.getMessager();
    this.trees = JavacTrees.instance(processingEnv);
    Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
    this.treeMaker = TreeMaker.instance(context);
    this.names = Names.instance(context);
  }
  
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
 
{
    // 获取自定义GetterTest注解的类
    Set<? extends Element> elementsAnnotatedWith = 
   roundEnv.getElementsAnnotatedWith(GetterTest.class);
    elementsAnnotatedWith.forEach(e -> {
      JCTree tree = trees.getTree(e);
        tree.accept(new TreeTranslator() {
          @Override
          public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
            List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
              // 在抽象树中找出所有的成员变量
              for (JCTree jcTree : jcClassDecl.defs) {
                if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                  JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                    jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                 }
              }
              // 对于变量进行生成getter方法的操作
              jcVariableDeclList.forEach(jcVariableDecl -> {
                messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + 
         has been processed"
);
                  jcClassDecl.defs = 
          jcClassDecl.defs.prepend(createGetterMethod(jcVariableDecl));
              });
              super.visitClassDef(jcClassDecl);
              }
          });
      });
      return true;
  }
  
  private JCTree.JCMethodDecl createGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
   
    // 生成表达式 this.name = name
    JCTree.JCExpressionStatement aThis = 
   makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")),               jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));        
    statements.append(aThis);
    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

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

    // 生成返回对象 void
    JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());

       //生成方法名 getName
    Name methodName = getMethodName(jcVariableDecl.getName());

    // 返回语法树对象
  return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),methodName, methodType,   
  List.nil(),parameters, List.nil(), block, null);
  
  }
  
  /**
   * 驼峰方法名
   */

  private Name getMethodName(Name name) {
    String s = name.toString();
    return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, 
   name.length()));
  }
  
  private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, 
  JCTree.JCExpression rhs)
 
{
    return treeMaker.Exec(treeMaker.Assign(lhs, rhs));
  }
}

2.4.4 getter-use工程

pom 文件 引入getter工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>getter-use</artifactId>

  <dependencies>
    <dependency>
    <groupId>com.example</groupId>
    <artifactId>getter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>
</project>

PersonDTO 使用@GetterTest

@GetterTest
public class PersonDTO {
  private String name;

  private Integer age;
}

2.5.5 项目进行编译

getter 模块下自动生成注册器

图片

getter-use 模块下 PersonDTO.class 可见生成了对应属性的 get 方法

图片

3.总结

本文通过以上对 Lombok 相关介绍,通过对 JAVA 文件编译过程分析和 JSR269 实现的方式, 基于这个规范然后引申出 Lombok 实现原理过程介绍,以及手动实现 getter 案例,想必我们对 Lombok 原理也有了相应的了解。虽然 Lombok 提供了许多便利,由于生成的代码不在源文件中可见,就会导致代码的可读性和维护性较差。在工作中 Lombok 使用时注意闭坑:

问题解决
@Data 和 @Builder 一起使用时,无参构造方法会被干掉手动加上注解: @AllArgsConstructor、@NoArgsConstructor。
@Builder 导致类属性默认值无效。有默认值属性上加注解 : @lombok.Builder.Default。
@Data 生成的 toString 方法,默认能不输出父类属性子类添加: @ToString(callSuper = true)。

参考文献

Lombok 官网地址: https://projectlombok.org

JavaParser 源码地址: https://github.com/javaparser/javaparser

JAVA 抽象语法树 AST 浅析与使用: https://www.freesion.com/article/4068581927/

看完两件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事

1.点个「在看」,让更多人也能看到这篇内容(点了「在看」,bug -1 😊)

2.关注公众号「政采云技术」,持续为你推送精选好文

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

继续滑动看下一个
政采云技术
向上滑动看下一个