APT 注解处理器如何实现Lombok的常用注解功能?带你完整解析

news2025/7/22 15:14:09

1 背景

在开发中我们常常会用到类似 lombok 、mapstruct 或者 mybatisplus 的框架,只要加入几个注解即可生成对应的方法,既然被很多框架使用,了解其中的原理还是非常有必要的。

2 生成字节码原理

2.1 APT(Annotation Processing Tool )注解处理器

基于 JSR 269(Pluggable Annotation Processing API)规范,提供插入式注解处理接口,Java 6 开始支持,它的主要功能是在 Java 编译期对源码进行处理, 通过这些规范插件,可以读取、修改、添加抽象语法树中的任意元素。

如上图 Javac 在编器期间,如果使用注解处理器对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直至处理完成,再对语法树进行修改。

2.2 AbstractProcessor 注解处理器的使用

创建一个注解处理器分为如下几步:

  • 创建注解类 : 比如 @Data 类
  • 创建 AbstractProcessor 的继承类, APT 的核心类
  • 修改生成字节码
  • SPI配置: 在 META-INF\services创建名为 javax.annotation.processing.Processor 配置文件添加 SPI 实现

2.3 APT 、 AOP、 JavaAgent 优缺点

在我们日常开发中,如果需要做一些埋点,AOP 并非唯一选择,APT 在有些场景下也可以使用的,支持静态方法和私有方法,同时稳定性也比较好,覆盖的场景比较全。

2.4 lombok 原理

1 APT(Annotation Processing Tool )注解处理器 2 javac api处理AST(抽象语法树)

大致原理如下图所示:

如想具体分析 lombok 的实现,可以从 Processor 和AnnotationProcessor 这两个类的 process 方法入手,通过 lombok.javac.JavacAnnotationHandler 处理器找到对应的注解实现。

3 自己实现Lombok

3.1 创建Data注解

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

该 Data 注解只能在编译期的时候获取到,在运行期是无法获取到的。

3.2 自定义注解处理器

通过实现Processor 接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor 类实现自定义注解处理器, 实现抽象方法 process 处理我们想要的功能。

3.2.1 APT简单介绍

@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataProcessor extends AbstractProcessor {
   @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    }
}

@SupportedAnnotationTypes 注解表示哪些注解需要注解处理器处理,可以多个注解校验 @SupportedSourceVersion 注解 用于指定jdk使用版本

如果不使用注解也可以在重写父类方法

Set<String> getSupportedAnnotationTypes() 
SourceVersion getSupportedSourceVersion
...
  • init 方法

主要是用于初始化上下文等信息

  • process方法

具体处理注解的业务方法

3.2.2 具体实现

  • 1 重写init方法
/**
   * 抽象语法树
   */
  private JavacTrees trees;
  /**
   * AST
   */
  private TreeMaker treeMaker;
  /**
   * 标识符
   */
  private Names names;
  /**
   * 日志处理
   */
  private Messager messager;
  private Filer filer;
  public synchronized void init(ProcessingEnvironment processingEnvironment) {
      super.init(processingEnvironment);
      this.trees = JavacTrees.instance(processingEnv);
      Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
      this.treeMaker = TreeMaker.instance(context);
      messager = processingEnvironment.getMessager();
      this.names = Names.instance(context);
      filer = processingEnvironment.getFiler();
  }

基本成员变量说明:

  • 1 JavacTrees 这个是当前的java语法树变量
  • 2 TreeMaker 这个是创建或修改方法的AST变量
  • 3 Names 这个是获取变量用的
  • 4 Messager 这个是打印日志的变量
  • 5 Filer 做一些过滤使用的

注: 使用AST语法需要使用本地包 tools.jar 包

<dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>1.8</version>
        <scope>system</scope>
        <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>
  • 2 重写process方法
@Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       Set<? extends Element> annotation = roundEnv.getElementsAnnotatedWith(Data.class);
       annotation.stream().map(element -> trees.getTree(element)).forEach(tree -> tree.accept(new TreeTranslator() {
           @Override
           public void visitClassDef(JCClassDecl jcClass) {
               //过滤属性
               Map<Name, JCVariableDecl> treeMap =
                   jcClass.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
                       .map(tree -> (JCVariableDecl)tree)
                       .collect(Collectors.toMap(JCVariableDecl::getName, Function.identity()));
               //处理变量
               treeMap.forEach((k, jcVariable) -> {
                   messager.printMessage(Diagnostic.Kind.NOTE, String.format("fields:%s", k));
                   try {
                       //增加get方法
                       jcClass.defs = jcClass.defs.prepend(generateGetterMethod(jcVariable));
                       //增加set方法
                       jcClass.defs = jcClass.defs.prepend(generateSetterMethod(jcVariable));
                   } catch (Exception e) {
                       messager.printMessage(Diagnostic.Kind.ERROR, Throwables.getStackTraceAsString(e));
                   }
               });
               //增加toString方法
               jcClass.defs = jcClass.defs.prepend(generateToStringBuilderMethod());
               super.visitClassDef(jcClass);
           }
           @Override
           public void visitMethodDef(JCMethodDecl jcMethod) {
               //打印所有方法
               messager.printMessage(Diagnostic.Kind.NOTE, jcMethod.toString());
               //修改方法
               if ("getTest".equals(jcMethod.getName().toString())) {
                   result = treeMaker
                       .MethodDef(jcMethod.getModifiers(), getNameFromString("testMethod"), jcMethod.restype,
                           jcMethod.getTypeParameters(), jcMethod.getParameters(), jcMethod.getThrows(),
                           jcMethod.getBody(), jcMethod.defaultValue);
               }
               super.visitMethodDef(jcMethod);
           }
       }));
       return true;
   }

上面逻辑分别实现了getter方法 setter方法 toString方法

大致逻辑:

1 过滤包含Data的 Element 变量 2 根据 Element 获取AST语法树 3 创建语法翻译器重写 visitClassDef 和 visitMethodDef 方法 4 过滤变量生成 get方法 set方法 和 toString方法

  • 3 get方法实现
private JCMethodDecl generateGetterMethod(JCVariableDecl jcVariable) {
      //修改方法级别
      JCModifiers jcModifiers = treeMaker.Modifiers(Flags.PUBLIC);
      //添加方法名称
      Name methodName = handleMethodSignature(jcVariable.getName(), "get");
      //添加方法内容
      ListBuffer<JCStatement> jcStatements = new ListBuffer<>();
      jcStatements.append(
          treeMaker.Return(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), jcVariable.getName())));
      JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
      //添加返回值类型
      JCExpression returnType = jcVariable.vartype;
      //参数类型
      List<JCTypeParameter> typeParameters = List.nil();
      //参数变量
      List<JCVariableDecl> parameters = List.nil();
      //声明异常
      List<JCExpression> throwsClauses = List.nil();
      //构建方法
      return treeMaker
          .MethodDef(jcModifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);
  }
  • 4 set方法实现
private JCMethodDecl generateSetterMethod(JCVariableDecl jcVariable) throws ReflectiveOperationException {
    //修改方法级别
    JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
    //添加方法名称
    Name variableName = jcVariable.getName();
    Name methodName = handleMethodSignature(variableName, "set");
    //设置方法体
    ListBuffer<JCStatement> jcStatements = new ListBuffer<>();
    jcStatements.append(treeMaker.Exec(treeMaker
        .Assign(treeMaker.Select(treeMaker.Ident(getNameFromString("this")), variableName),
            treeMaker.Ident(variableName))));
    //定义方法体
    JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
    //添加返回值类型
    JCExpression returnType =
        treeMaker.Type((Type)(Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));
    List<JCTypeParameter> typeParameters = List.nil();
    //定义参数
    JCVariableDecl variableDecl = treeMaker
        .VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariable.name, jcVariable.vartype, null);
    List<JCVariableDecl> parameters = List.of(variableDecl);
    //声明异常
    List<JCExpression> throwsClauses = List.nil();
    return treeMaker
        .MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);
}
  • 5 toString方法实现
private JCMethodDecl generateToStringBuilderMethod() {
       //修改方法级别
       JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
       //添加方法名称
       Name methodName = getNameFromString("toString");
       //设置调用方法函数类型和调用函数
       JCExpressionStatement statement = treeMaker.Exec(treeMaker.Apply(List.of(memberAccess("java.lang.Object")),
           memberAccess("com.nicky.lombok.adapter.AdapterFactory.builderStyleAdapter"),
           List.of(treeMaker.Ident(getNameFromString("this")))));
       ListBuffer<JCStatement> jcStatements = new ListBuffer<>();
       jcStatements.append(treeMaker.Return(statement.getExpression()));
       //设置方法体
       JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());
       //添加返回值类型
       JCExpression returnType = memberAccess("java.lang.String");
       //参数类型
       List<JCTypeParameter> typeParameters = List.nil();
       //参数变量
       List<JCVariableDecl> parameters = List.nil();
       //声明异常
       List<JCExpression> throwsClauses = List.nil();
       return treeMaker
           .MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);
   }
private JCExpression memberAccess(String components) {
       String[] componentArray = components.split("\\.");
       JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
       for (int i = 1; i < componentArray.length; i++) {
           expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
       }
       return expr;
   }
   private Name handleMethodSignature(Name name, String prefix) {
       return names.fromString(prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, name.toString()));
   }
   private Name getNameFromString(String s) {
       return names.fromString(s);
   }

最后是通过 SPI 的方式加载注解处理器,spi 可以用 java 自带的方式,具体用法可以参考我的文章:框架基础之SPI机制 , 这里我们使用 google 封装的 auto-service 框架来实现。

在pom文件中引入

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc4</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.google.auto</groupId>
    <artifactId>auto-common</artifactId>
    <version>0.10</version>
    <optional>true</optional>
</dependency>

然后在添加AutoService注解

@SupportedAnnotationTypes({"com.nicky.lombok.annotation.Data"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class DataProcessor extends AbstractProcessor {
}

最后就是 mvn clean install打包到本地仓库作为一个公共包

[INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/target/java-feature.jar to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.jar
[INFO] Installing /Users/chenxing/Documents/sourcecode/id-generator-spring-boot-starter/lombok-enchance/pom.xml to /Users/chenxing/m2repository/com/nicky/lombok-enchance/1.0.4/lombok-enchance-1.0.4.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.372 s
[INFO] Finished at: 2022-09-03T10:44:27+08:00
[INFO] ------------------------------------------------------------------------
➜  lombok-enchance git:(master) ✗ 

我们测试下,我们的注解处理器是否按所想的那样,实现了相应功能。

在项目中引入本地依赖 例如我的仓库依赖坐标:

<dependency>
    <groupId>com.nicky</groupId>
    <artifactId>lombok-enchance</artifactId>
    <version>1.0.4</version>
</dependency>

给LombokTest 类添加 @Data 注解

@Data
public class LombokTest {
    private String name;
    private int age;
    public LombokTest(String name) {
        this.name = name;
    }
    public static void main(String[] args) {
        LombokTest lombokTest = new LombokTest("nicky");
        lombokTest.age = 18;
        System.out.println(lombokTest.toString());
    }
}

我们编译上面的类,查看 class文件是否生成了getField() setField() toString()方法

public class LombokTest {
    private java.lang.String name;
    private int age;
    public java.lang.String toString() { /* compiled code */ }
    public void setName(java.lang.String name) { /* compiled code */ }
    public java.lang.String getName() { /* compiled code */ }
    public void setAge(int age) { /* compiled code */ }
    public int getAge() { /* compiled code */ }
    public LombokTest(java.lang.String name) { /* compiled code */ }
    public static void main(java.lang.String[] args) { /* compiled code */ }
}

成功啦 😁

最后测试下main方法

打印结果如下:

{“name”:“清水”,“age”:18}

说明toString方法生效了。

当然对于 get 和 set 方法 直接在IDE工具里还是无法调用的,需要编写 IDE 的 Lombok 插件,这里就不去扩展了。

Reference

  • 在编译期修改语法树
  • tools.jar注释文档
  • JSR-269

以上就是APT 注解处理器实现 Lombok 常用注解功能详解的详细内容,更多关于APT 实现Lombok注解功能的资料请点击获取《Android核心技术笔记》或查看主页其它相关文章!

文末

spi全称为 (Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,一种解耦非常优秀的思想。

spi的工作原理: 就是ClassPath路径下的META-INF/services文件夹中, 以接口的全限定名来命名文件名,
文件里面写该接口的实现。然后再资源加载的方式,读取文件的内容(接口实现的全限定名), 然后再去加载类。

spi可以很灵活的让接口和实现分离, 让api提供者只提供接口, 第三方来实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文笔记】用于几何匹配的卷积神经网络结构(CNN for Geometric Matching)

用于几何匹配的卷积神经网络结构(CNN for Geometric Matching) 文章目录AbstractCNNRelated WorkArchitecture for Geometric Matching特征提取&#xff1a;Matching Networkcorrelation-layer归一化&#xff1a;Regression NetworkGeometric TransformationsAffine Transforma…

正大杯市场调查与分析大赛|赛前准备 持续更新ing

目录 1.随机化实验 1.1背景 1.2操作 1.3作用 平衡协变量 克服选择偏差 1.4分类 完全随机化实验 分层随机化实验 配对随机化实验 1.5分层随机化 1.6缺陷 2.均值、中位数、分割点、游程数 ​3. 什么是系统抽样 4.F大于临界值说明什么 5.德宾—沃森(DW)统计量[Dur…

webpack5 CssMinimizerPlugin css压缩

CssMinimizerWebpackPlugin | webpack 中文文档webpack 是一个模块打包器。它的主要目标是将 JavaScript 文件打包在一起&#xff0c;打包后的文件用于在浏览器中使用&#xff0c;但它也能够胜任转换&#xff08;transform&#xff09;、打包&#xff08;bundle&#xff09;或包…

动态修改el-input样式;动态修改elmentUI元素样式;css变量

场景&#xff1a;正常我们动态修改div元素的样式&#xff0c;使用:style和:class即可&#xff1b;但是我们想要动态修改element的组件样式时候&#xff0c;例如el-input字体颜色&#xff0c;由于el-input的样式嵌套很深&#xff0c;我们需要修改的实际是.el-input__inner这个样…

强强合作,替代钉盘/微盘,企业实现低成本扩容

后疫情时代&#xff0c;远程办公不断推动协同办公软件快速发展&#xff0c;协同办公软件活跃度保持增长&#xff0c;无疑不反应出企业对对此类办公产品的喜欢&#xff0c;并保持深度使用。 客户的需求和选择也不是一成不变的&#xff0c;完美日记曾经是企业微信的重点客户&…

【附源码】计算机毕业设计JAVA互联网保险网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven Vue 等等组成&#xff0c;B/…

9类人事管理场景应用,泛微协助HR释放更多工作量

人事管理数字化是大势所趋&#xff1a; 组织处在不断发展变化的大环境之中&#xff0c;竞争格外激烈&#xff0c;人力资源日益成为组织竞争优势的关键所在。改变传统人事管理模式&#xff0c;推进人事管理数字化变革&#xff0c;把传统的人事管理事务统一到一个平台&#xff0…

从零到一掌握 Docker 的基本原理与实践操作

富 Web 时代,应用变得越来越强大,与此同时也越来越复杂。集群部署、隔离环境、灰度发布以及动态扩容缺一不可,而容器化则成为中间的必要桥梁。本文我们就来探索Docker的神秘世界,从零到一掌握 Docker 的基本原理与实践操作,是时候该开疆扩土啦。 讲个故事 为了更好的理解 …

【BSC】使用Python实现PancakeSwap自动交易(入门篇)

需求 最近我们需要在BSC上实现代币的自动化交易&#xff0c;比如自动把BNB兑换成USDT&#xff0c;自动把USDT兑换成CAKE等其它代币&#xff0c;同时也要监视价格&#xff0c;在价格合适的时候再兑换代币。而PancakeSwap正是BSC上最大的去中心化交易平台&#xff0c;我们已经学…

2022行情变了,请各位做好一年内随时失业的准备

前两天跟一个HR朋友聊天&#xff0c;她表示刚在boss上发布了一个普通测试岗位&#xff0c;不到一小时竟然收到了几百份简历。而且简历质量极高&#xff0c;这是往年不敢想象的。岗位少&#xff0c;竞争激烈&#xff0c;这是今年软件测试就业的真实写照&#xff0c;也是所有岗位…

【附源码】计算机毕业设计JAVA畜牧场信息管理系统

【附源码】计算机毕业设计JAVA畜牧场信息管理系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA …

个人如何调用股票程序交易接口?

关于股票程序交易接口的调用一般只有量化交易投资者才能涉及的到&#xff0c;那么做常见也最实用的方法就是使用外接端口&#xff0c;意思就是可以买一个接口就可以接入交易软件了&#xff0c;不过要具有编程基础才能实现&#xff0c;因为现有的策略和设置的函数都是无法满足投…

OSI七层模型

介绍 七层模型&#xff0c;亦称OSI&#xff08;Open System Interconnection&#xff09;。参考模型是国际标准化组织&#xff08;ISO&#xff09;制定的一个用于计算机或通信系统间互联的标准体系&#xff0c;一般称为OSI参考模型或七层模型。 分层 应用层 提供高级api …

HDFS 基本 shell 操作

HDFS 基本 shell 操作1.1 创建目录1.2 上传指令1.3 创建空文件1.4 向分布式文件系统中的文件里追加内容1.5 查看指令1.6 下载指令1.7 合并下载1.8 移动hdfs中的文件1.9 复制hdfs中的文件到hdfs的另一个目录1.10 删除命令1.11 查看磁盘利用率和文件大小1.12 修改权限1.13 修改文…

子进程信号继承;kill+raise+alarm+pause+信号发生接收和处理+信号屏蔽

子进程对父进程信号继承情况 fork创建子进程&#xff0c;但子进程没有exec 在fork子进程之前: 如果父进程调用signal设置了某个信号的处理方式的话&#xff0c;那么fork出的子进程会继承父进程对该信号设置的处理 强调:只有在fork之前&#xff0c;父进程所设置的信号处理方式&…

餐厅扫码点餐怎么弄_分享扫码点餐小程序开发制作方法

目前市场上有很多扫码点餐的小程序系统&#xff0c;制作方法有三种&#xff1a; 1、使用微信商家推出的扫码点餐小程序&#xff0c;上传营业执照和食品经营许可证就可以开通使用&#xff0c;然后上传自己的菜品信息就可以了&#xff0c;功能相对比较简单。 2、购买餐饮系统公司…

Redis数据持久化(持久化过程中写操作如何处理)

上一节简单分析了Redis数据持久化方式&#xff0c;点击这里查看。但是要考虑的一个问题就是&#xff0c;在Redis持久化的时候&#xff0c;有新的写入指令时&#xff0c;Redis是如何操作的。 1.RDB方式 1.1.同步方式 即save操作&#xff0c;在Redis执行save操作时&#xff0c;…

systemVerilog的变量类型转换

1 Type Casting 1.1 Verilog使用赋值语句进行变量类型转换 阻塞赋值&#xff1a; 非阻塞赋值&#xff1a; < 1.2 systemVerilog增加了变量类型转换符 变量类型转换符可以在任何时刻对表达式进行类型转换而不像Verilog一样只能发生在赋值语句中 logicint a&#xff0c;y&…

开放式运动耳机怎么样,几款合适的骨传导耳机推荐

骨传导作为新时代的产物&#xff0c; 近些年在耳机市场也是兴起了一波热潮&#xff0c;无论是在日常出勤时佩戴&#xff0c;还是在运动的时候佩戴&#xff0c;骨传导耳机相对于传统耳机来说无疑是越来越符合。其骨传导最大的特点无非就是特殊的传声方式以及无需入耳的佩戴&…

每日一个设计模式之【外观模式】

文章目录每日一个设计模式之【外观模式】☁️前言&#x1f389;&#x1f389;&#x1f389;&#x1f33b;外观模式概述&#x1f331;外观模式的实现&#x1f332;总结每日一个设计模式之【外观模式】 ☁️前言&#x1f389;&#x1f389;&#x1f389; 大家好✋&#xff0c;我…