我的编程空间,编程开发者的网络收藏夹
学习永远不晚

APT 注解处理器实现 Lombok 常用注解功能详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

APT 注解处理器实现 Lombok 常用注解功能详解

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 的实现,可以从 ProcessorAnnotationProcessor 这两个类的 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;
    
    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 创建语法翻译器重写 visitClassDefvisitMethodDef 方法 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() {  }
    public void setName(java.lang.String name) {  }
    public java.lang.String getName() {  }
    public void setAge(int age) {  }
    public int getAge() {  }
    public LombokTest(java.lang.String name) {  }
    public static void main(java.lang.String[] args) {  }
}

成功啦 ?

最后测试下main方法

打印结果如下:

{"name":"清水","age":18}

说明toString方法生效了。

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

Reference

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

以上就是APT 注解处理器实现 Lombok 常用注解功能详解的详细内容,更多关于APT 实现Lombok注解功能的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

APT 注解处理器实现 Lombok 常用注解功能详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

SpringBoot使用@Cacheable注解实现缓存功能流程详解

最近一直再学SpringBoot,在学习的过程中也有过很多疑问。为了解答自己的疑惑,也在网上查了一些资料,以下是对@Cacheable注解的一些理解
2023-01-12

使用注解实现Redis缓存功能

本文实例为大家分享了使用注解实现Redis缓存功能的具体代码,供大家参考,具体内容如下非关系型内存数据库,有持久化操作,C语言编写的key,value存储系统(区别于mysql的二维表格的形式存储。)rdb:周期性的持久化aof:以
2022-07-28

微信小程序登录与注册功能的实现详解

在小程序的开发中,可能起点就是用户的登录与注册了。和粉丝的互动过程中发现,对于用户的登录注册还是没有彻底掌握。我们本篇就来分析一下登录与注册如何实现
2022-11-13

怎么使用Spring注解实现循环重试功能

这篇文章主要介绍“怎么使用Spring注解实现循环重试功能”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么使用Spring注解实现循环重试功能”文章能帮助大家解决问题。一、@Retryable是什
2023-07-05

详解SpringBoot中的统一功能处理的实现

这篇文章主要为大家详细介绍了SpringBoot如何实现统一功能处理,文中的示例代码讲解详细,对我们学习或工作有一定借鉴价值,需要的可以参考一下
2023-01-28

使用Spring注解怎么实现Bean自动装配功能

这篇文章将为大家详细讲解有关使用Spring注解怎么实现Bean自动装配功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。使用须知:1.导入约束:context约束2.配置注解的支持: co
2023-06-14

SpringMVC整合SSM实现异常处理器详解

SpringMVC是一种基于Java,实现了WebMVC设计模式,请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将Web层进行职责解耦。基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,SpringMVC也是要简化我们日常Web开发
2022-11-13

使用注解怎么实现一个SpringBoot 接口防刷功能

这篇文章将为大家详细讲解有关使用注解怎么实现一个SpringBoot 接口防刷功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。项目结构如下:一、编写注解类 AccessLimitpacka
2023-06-06

Spring注解实现循环重试功能(适用场景分析)

这篇文章主要介绍了Spring注解实现循环重试功能,本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景和注意事项,当需要重试的时候还是很有用的,需要的朋友可以参考下
2023-05-15

SQL实现Excel的10个常用功能的示例详解

目录01. 关联公式:Vlookup02. 对比两列差异03. 去除重复值04. 缺失值处理05. 多条件筛选06. 模糊筛选数据07. 分类汇总08. 条件计算09. 删除数据间的空格10. 合并与排序列SQL笔试题原题某数据服务公司某手
2022-07-25

iOS Swift利用UICollectionView实现无限轮播功能(原理)详解

前言 作为一个资深(自认为)iOS程序猿,会经常用到轮播图,上一次使用UIScrollView实现无限轮播的效果,这一次在Swift语言中,我使用UICollectionView再为大家讲解一次无限轮播的实现原理。 先上图:UICollec
2022-05-27

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录