JSR 269插件化注解处理实践(以模拟Lombok自动生成getter/setter为例)

简介: JSR 269是jdk1.6中引入的,在此之前,开发者只能在运行期对注解进行处理,而JSR 269允许开发这在编译期对注解进行处理,以此可以完成很多java不支持的特性,甚至创造新的语法糖。大名鼎鼎的lombok就是基于JSR 269来实现的。1. 原理介绍javac的编译过程如下图所示:可以看到JSR 269,也就是注解处理在编译产生抽象语法树AST之后,在此处插入一些逻辑,并且可以对AST进行

JSR 269是jdk1.6中引入的,在此之前,开发者只能在运行期对注解进行处理,而JSR 269允许开发这在编译期对注解进行处理,以此可以完成很多java不支持的特性,甚至创造新的语法糖。大名鼎鼎的lombok就是基于JSR 269来实现的。

1. 原理介绍

javac的编译过程如下图所示:

可以看到JSR 269,也就是注解处理在编译产生抽象语法树AST之后,在此处插入一些逻辑,并且可以对AST进行修改。因为JSR 269注解处理是在attr(语义分析,各种静态检查都是在这里做的)之前的,所以我们甚至可以将一颗不符合java语义的AST处理成符合意义的AST,从而完成一些cool operation。

jdk提供了一套操作抽象语法树的API,有三个核心类

  • Names类提供访问AST中标识符的方法。
  • 例如访问this标识符,name.fromString("this")
  • JCTree是代表各种AST节点的基类,其有很多子类,如JCStatement,JCExpression,JCMethodDecl,JCModifier等。
  • JCStatement是用来代表java声明语句的,它也有很多子类,如JCReturn,JCBlock(AST中的代码块节点,对应到java中就是中括号包含的一段代码),JCClassDecl(AST中的类声明节点),JCVariableDecl(AST中的变量声明节点),JCTry,JCIf,JCFroLoop(AST中的循环语句节点)等。
    • JCExpression用来代表表达式语句节点,常用的子类有JCAssign(赋值语句,如x=1),JCIdent(标识符语句),JCBinary(二元运算语句,如1+2),JCLiteral(常量语句)。
    • JCMethodDecl用于代表方法定义

  • JCModifiers代表AST访问标记节点,其有一个flag属性,取值如下:

  • TreeMaker封装了操作AST节点的方法。其拥有丰富的创建各种AST的方法,常用的有
  • Binary,生成二元表达式节点,如1+2:

treeMaker.Binary(JCTree.Tag.PLUS,  treeMaker.Literal(1),  treeMaker.Literal(2));

  • Ident,用于创建标识符节点,如获取标识符x:

treeMaker.Ident(names.fromString("x"));

  • Select,创建一个字段或方法,或访问某个Field,如this.id对应的Select代码为

treeMaker.Select(treeMaker.Ident(names.fromString("this"))names.fromString("id"));

  • Assign,生成一个赋值语句节点,如x=0:

treeMaker.Assign(     treeMaker.Ident(names.fromString("x"))//左表达式     treeMaker.Literal(0//右表达式 );

  • Exec,用于包装JCAssign对象,使其返回一个JCExpressionStatement,JCExpressionStatement就可以直接用来生成方法AST节点了,如下

treeMaker.Exec(     treeMaker.Assign(         treeMaker.Ident(names.fromString("x"))//左表达式         treeMaker.Literal(0//右表达式     ) );

2. 自定义MyData注解实现getter/setter自动生成

下面来模拟lombok的Data注解的实现,来看一下JSR 269应该如何进行实践。

先声明MyData注解。

package com.xycode.techlecture.jsr269;

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

/**
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/7/12
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface MyData {
}

接下来创建注解处理器类,jdk中有一个AbstractProcessor,我们只需继承它,然后编写字节的注解处理逻辑,目前是使得标注了MyData注解的类能够自动支持getter、setter方法。如下:

package com.xycode.techlecture.jsr269;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.Tree.Kind;
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.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;

/**
 * 注解处理器实现(编译期执行), 实现类型lombok的@Data注解功能
 *
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/7/12
 */
//SupportedAnnotationTypes用于指定改processor支持的注解
@SupportedAnnotationTypes("com.xycode.techlecture.jsr269.MyData")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyDataAnnotationProcessor extends AbstractProcessor {

    /**
     * 描述语法树的实例类
     */
    private JavacTrees javacTrees;

    /**
     * 创建语法树节点的工具类
     */
    private TreeMaker treeMaker;

    /**
     * 访问语法树中的标识符
     * eg: names.fromString("str")
     */
    private Names names;

    /**
     * 从AST上下文中初始化JavacTrees,TreeMaker与Names
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Context context = ((JavacProcessingEnvironment)processingEnv).getContext();

        javacTrees = JavacTrees.instance(processingEnv);

        treeMaker = TreeMaker.instance(context);

        names = Names.instance(context);

    }

    /**
     * 生成getter方法节点
     */
    private JCTree.JCMethodDecl genGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
        Name variableDeclName = jcVariableDecl.getName();
        //生成语句: return this.xxx
        JCTree.JCReturn returnStatement = treeMaker.Return(
            treeMaker.Select(
                treeMaker.Ident(names.fromString("this")), variableDeclName
            )
        );
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<JCTree.JCStatement>().append(returnStatement);

        //生成public修饰符
        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        //拼接方法名 getXxx
        String getMethodNameStr = "get" + variableDeclName.toString().substring(0, 1).toUpperCase()
            + variableDeclName.toString().substring(1);
        Name getMethodName = names.fromString(getMethodNameStr);

        //生成返回类型标识
        JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;

        //生成方法体
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        //生成泛型参数列表
        com.sun.tools.javac.util.List<JCTree.JCTypeParameter> methodGenericParameterList
            = com.sun.tools.javac.util.List.nil();

        //生成参数值列表
        com.sun.tools.javac.util.List<JCTree.JCVariableDecl> methodParameterValList
            = com.sun.tools.javac.util.List.nil();

        //生成抛出的异常列表
        com.sun.tools.javac.util.List<JCTree.JCExpression> throwExceptionList = com.sun.tools.javac.util.List.nil();

        //生成方法定义AST节点
        return treeMaker.MethodDef(modifiers,  //public
            getMethodName, //方法名: getXxx
            returnMethodType, //返回类型
            methodGenericParameterList, //泛型参数列表
            methodParameterValList, //参数值列表
            throwExceptionList, //抛出异常列表
            body, //方法体
            null);
    }

    /**
     * 生成setter方法节点
     */
    private JCTree.JCMethodDecl genSetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
        Name variableDeclName = jcVariableDecl.getName();
        //生成语句: this.xxx = xxx
        JCTree.JCExpressionStatement statement = treeMaker.Exec(
            treeMaker.Assign(
                treeMaker.Select(
                    treeMaker.Ident(names.fromString("this")), variableDeclName
                ),//左表达式部分: this.xxx
                treeMaker.Ident(variableDeclName) //右表达式部分: xxx
            )
        );
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<JCTree.JCStatement>().append(statement);

        //setter方法参数
        JCTree.JCVariableDecl param = treeMaker.VarDef(
            treeMaker.Modifiers(Flags.PARAMETER, com.sun.tools.javac.util.List.nil()), //访问修饰符
            variableDeclName, //变量名字
            jcVariableDecl.vartype, //变量类型
            null //变量初始值
        );

        //生成public修饰符
        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        //拼接方法名 getXxx
        String getMethodNameStr = "set" + variableDeclName.toString().substring(0, 1).toUpperCase()
            + variableDeclName.toString().substring(1);
        Name getMethodName = names.fromString(getMethodNameStr);

        //生成返回类型标识 void
        JCTree.JCExpression returnMethodType = treeMaker.Type(new Type.JCVoidType());

        //生成方法体
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        //生成泛型参数列表
        com.sun.tools.javac.util.List<JCTree.JCTypeParameter> methodGenericParameterList
            = com.sun.tools.javac.util.List.nil();

        //生成参数值列表
        com.sun.tools.javac.util.List<JCTree.JCVariableDecl> methodParameterList = com.sun.tools.javac.util.List.of(
            param);

        //生成抛出的异常列表
        com.sun.tools.javac.util.List<JCTree.JCExpression> throwExceptionList = com.sun.tools.javac.util.List.nil();

        //生成方法定义AST节点
        return treeMaker.MethodDef(modifiers,  //public
            getMethodName, // 方法名: setXxx
            returnMethodType, //返回类型
            methodGenericParameterList, //泛型参数列表
            methodParameterList, //参数类型列表
            throwExceptionList, //抛出异常列表
            body, //方法体
            null);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //获取标注了MyData注解的元素, 这里实际上只有类元素
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MyData.class);

        for (Element element : set) {
            //获取标注了MyData注解的类的语法树
            JCTree tree = javacTrees.getTree(element);

            //notice: 解决编译错误【java.lang.AssertionError: Value of x -1】
            // 因为treeMaker.pos的值是不会变的(=-1), 所以在遍历是需要实时更新
            treeMaker.pos = tree.pos;

            //遍历语法树(在遇到visitClassDef事件, 也就是访问到类定义节点时去修改语法树节点)
            tree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCClassDecl jcClassDecl) {
                    //获取定义在该类下的所有元素(成员变量, 方法等)
                    jcClassDecl.defs.stream()
                        //过滤出变量类型的元素
                        .filter(o -> o.getKind().equals(Kind.VARIABLE))
                        //强制转换元素为变量类型元素
                        .map(o -> ((JCTree.JCVariableDecl)o))
                        //遍历处理每个变量元素
                        .forEach(o -> {
                            //为类定义节点新增getter方法
                            jcClassDecl.defs = jcClassDecl.defs.prepend(genGetterMethod(o));

                            //为类定义节点新增setter方法
                            jcClassDecl.defs = jcClassDecl.defs.prepend(genSetterMethod(o));
                        });
                    //修改类节点完毕
                    super.visitClassDef(jcClassDecl);
                }
            });
        }

        return true;
    }

}

接下来声明一个User类,如下:

package com.xycode.techlecture.jsr269;

/**
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/7/12
 */
@MyData
public class User {

    private Integer id;

    private String name;

    public User() {
    }

    public static void main(String[] args) {
        User user = new User();
        user.setId(1234);
        user.setName("xycode");
        System.out.println(user.getId());
        System.out.println(user.getName());
    }
}

可以看到User类这里没有创建getter,setter方法,但是main中使用到了,显然正常情况下会编译报错,但是借助JSR 269的插件化注解处理能力,我们可以在编译期间介入AST的生成,从而完成getter,setter的正常调用,编译命令如下:

先编译MyData与MyDataAnnotationProcessor:

再编译User:

可以看出使能够编译成功的,反编译看一下User,如下

可以看出getter,setter方法已经正常生成了,执行如下:

执行也是ok的。

目录
相关文章
|
6天前
|
Java
lombok的使用
本文介绍了Lombok库的基本使用方法和常用注解,通过示例代码展示了如何使用Lombok简化Java对象的创建、属性访问、日志记录等编码工作,使代码更加简洁。
lombok的使用
|
7天前
|
Java API 开发者
探索Java中的Lambda表达式:简洁与强大的代码实践
本文深入探讨Java中Lambda表达式的定义、用法及优势,通过实例展示其如何简化代码、提升可读性,并强调在使用中需注意的兼容性和效率问题。Lambda作为Java 8的亮点功能,不仅优化了集合操作,还促进了函数式编程范式的应用,为开发者提供了更灵活的编码方式。
|
7天前
|
Java 程序员 数据库连接
Java中的异常处理机制:理解与实践
本文将深入探讨Java语言中异常处理的核心概念、重要性以及应用方法。通过详细解析Java异常体系结构,结合具体代码示例,本文旨在帮助读者更好地理解如何有效利用异常处理机制来提升程序的健壮性和可维护性。
|
1月前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
7天前
|
Java 编译器 程序员
Java注解,元注解,自定义注解的使用
本文讲解了Java中注解的概念和作用,包括基本注解的用法(@Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface),Java提供的元注解(@Retention, @Target, @Documented, @Inherited),以及如何自定义注解并通过反射获取注解信息。
Java注解,元注解,自定义注解的使用
|
12天前
|
Java 程序员
Java中的多线程基础与实践
【9月更文挑战第21天】本文旨在引导读者深入理解Java多线程的核心概念,通过生动的比喻和实例,揭示线程创建、同步机制以及常见并发工具类的使用。文章将带领读者从理论到实践,逐步掌握如何在Java中高效地运用多线程技术。
|
10天前
|
Java 调度 开发者
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java多线程编程的核心概念和实际应用,通过浅显易懂的语言解释多线程的基本原理,并结合实例展示如何在Java中创建、控制和管理线程。我们将从简单的线程创建开始,逐步深入到线程同步、通信以及死锁问题的解决方案,最终通过具体的代码示例来加深理解。无论您是Java初学者还是希望提升多线程编程技能的开发者,本文都将为您提供有价值的见解和实用的技巧。
15 2
|
11天前
|
Java 程序员 数据库连接
Java编程中的异常处理:理解与实践
【9月更文挑战第22天】在Java编程的世界里,异常处理是一项基础而关键的能力。它不仅关乎程序的健壮性,也体现了开发者对待不可预知情况的态度。本文将通过深入浅出的方式,带你认识Java的异常处理机制,从理论到实践,一步步解锁异常处理的秘密。
|
12天前
|
Java 数据处理
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java中的多线程编程,涵盖其基本概念、创建方法、同步机制及实际应用。通过对多线程基础知识的介绍和具体示例的演示,希望帮助读者更好地理解和应用Java多线程编程,提高程序的效率和性能。
19 1
|
14天前
|
IDE Java 编译器
lombok编译遇到“找不到符号的问题”
【9月更文挑战第18天】当使用 Lombok 遇到 “找不到符号” 的问题时,可能是由于 Lombok 未正确安装、编译器不支持、IDE 配置不当或项目构建工具配置错误。解决方法包括确认 Lombok 安装、编译器支持,配置 IDE 和检查构建工具配置。通过这些步骤通常可解决问题,若问题仍存在,建议检查项目配置和依赖,或查看日志获取更多信息。
下一篇
无影云桌面