栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

5. 实战:JCTree实现编译时注解处理器

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

5. 实战:JCTree实现编译时注解处理器

1. 絮絮叨叨

一开始学习APT时,自己的终极目标:能在一个已有的类内部,添加Builder类以实现Builder设计模式也就是说,自己希望通过学习APT修改已有的Java源码通过查阅资料,发现可以通过修改Java语法树(JCTree)实现Java源码的修改上一篇博客:4. JCTree相关知识学习,介绍了JCTree的相关知识此次,通过@Value注解来看看如果通过JCTree修改Java源码@Value注解的作用:为非final的String字段赋默认初始值因为本菜鸟认为,final字段应该显式赋值:声明时初始化或者通过构造函数初始化  2. 预备知识:如何获取注解中元素的值

按照之前的描述,@Value注解可以为非final的String字段赋默认初始值

@Value注解的定义如下,包含一个value元素,以设置字段的默认初始值

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Value {
    String value();
}

使用方法如下:

@Value("深圳")
private static String address;

问题来了,如何获取@Value中的值深圳,从而实现为address字段赋默初始认值?

自己就是个半灌水,每一步都需要查一下。感谢博客:利用 APT 在 Java 文件编译时获取注解信息,给了自己灵感

原来,Element提供了一个getAnnotation()方法,通过指定注解的Class类型,就可以获得对应的注解实例

拿到了注解实例,访问注解中元素的值就非常简单了。具体可以参考之前的博客中1.3.3.4节:1. Java注解

Value valueAn = element.getAnnotation(Value.class); // 获取@Value注解实例
value.value(); // 获取value
3. 代码实战 3.1 实现ValueProcessor

在annotation-processor模块,基于Google的auto-service,创建ValueProcessor

@AutoService(Processor.class)
@SupportedAnnotationTypes("sunrise.annotation.Value")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ValueProcessor extends AbstractProcessor {
    private static int round; // 用于标识注解处理的round
    private Messager messager;
    private Context context;  // 创建TreeMaker和Names所需的上下文
    private JavacTrees trees; // Java语法树的工具类
    private TreeMaker treeMaker; // 创建语法树节点的工厂类
    private Names names;  // 编译器名称表的访问,提供了一些标准的名称和创建新名称的方法

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.trees = JavacTrees.instance(processingEnv);
        this.treeMaker = TreeMaker.instance(context);
        this.names = new Names(context);
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, ValueProcessor.class.getSimpleName() + " round " + (++round));

        for (TypeElement annotation : annotations) {
            // 通过lambda表达式,处理被注解标记的每个元素
            roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
                // 获取value的值
                Value valueAnnotation = element.getAnnotation(Value.class);
                String value = valueAnnotation.value();

                // 修改语法树节点:直接修改init,为字段赋默认初始值
                JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) trees.getTree(element);
                if (!jcVariableDecl.getModifiers().getFlags().contains(Modifier.FINAL) && jcVariableDecl.vartype.toString().equals("String")) {
                    messager.printMessage(Diagnostic.Kind.NOTE, "原始的字段信息: " + jcVariableDecl.toString());
                    jcVariableDecl.init = treeMaker.Literal(value);
                    messager.printMessage(Diagnostic.Kind.NOTE, "修改后的字段信息: " + jcVariableDecl.toString());
                } else {
	                  messager.printMessage(Diagnostic.Kind.ERROR, "当前字段: " + jcVariableDecl.toString() + "n@Value注解只能作用于非final的String字段!");

                }
            });
        }
        return roundEnv.processingOver();
    }
}

通过 mvn clean install命令完成annotation-processor模块的安装

3.2 使用@Value注解

在annotation-use模块创建ValueProcessorTest类,使用@Value注解

public class ValueProcessorTest {
    @Value("mac os")
    public static String SYSTEM;
    @Value("张三")
    private String name;
    @Value("21")  // 错误的使用,是为了验证字段类型的限定是否生效
    private int age;

	public static void main(String[] args) {
       ValueProcessorTest test = new ValueProcessorTest();
        // 有初始值直接打印
        System.out.println("system: " + ValueProcessorTest.getSYSTEM() + ", name: " + test.getName() + ", age: "  + test.getAge());
    }
    // getter、setter方法省略
}

通过 mvn clean compile命令完成annotation-use模块的编译,编译报错。说明,注解处理器实现了字段类型的限定。

将age字段的@Value注解注释掉,成功完成编译。

通过IDEA查看target/classed目录中的ValueProcessorTest.class,内容如下

package sunrise.annotation.use;

public class ValueProcessorTest {
    public static String SYSTEM = "mac os";
    private String name = "张三";
    private int age;

    public static void main(String[] args) {
        ValueProcessorTest test = new ValueProcessorTest();
        System.out.println("system: " + getSYSTEM() + ", name: " + test.getName() + ", age: " + test.getAge());
    }
    // 省略默认构造函数、getter、setter方法
}

执行main方法,结果如下

不管是从反编译后的class文件,还是从执行结果,都说明:通过JCTree,成功实现了@Value注解

3.3 通过visitor模式为字段赋默认初始值

上面的process()方法中,直接通过修改JCVariableDecl的init字段,实现了为字段赋默认初始值,并未体会到vistor模式在JCTree中的作用

下面的process()方法,将通过visitor模式为字段赋默认初始值

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
    messager.printMessage(Diagnostic.Kind.NOTE, ValueProcessor.class.getSimpleName() + " round " + (++round));

    for (TypeElement annotation : annotations) {
        // 通过lambda表达式,处理被注解标记的每个元素
        roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
            // 获取value的值
            Value valueAnnotation = element.getAnnotation(Value.class);
            String value = valueAnnotation.value();
            // 通过visitor模式为字段赋默认初始值
            jcVariableDecl.accept(new TreeTranslator(){
                @Override
                public void visitVarDef(JCTree.JCVariableDecl tree) {
                    super.visitVarDef(tree);
                    if (!jcVariableDecl.getModifiers().getFlags().contains(Modifier.FINAL) && jcVariableDecl.vartype.toString().equals("String")) {
                        messager.printMessage(Diagnostic.Kind.NOTE, "原始的字段信息: " + jcVariableDecl.toString());
                        jcVariableDecl.init = treeMaker.Literal(value);
                        messager.printMessage(Diagnostic.Kind.NOTE, "修改后的字段信息: " + jcVariableDecl.toString());
                        // 更新语法树节点
                        this.result = jcVariableDecl;
                    } else {
                        messager.printMessage(Diagnostic.Kind.ERROR, "@Value注解只能作用于非final的String字段!");
                    }
                }
            });
        });
    }
    return roundEnv.processingOver();
}
4. 其他示例 4.1 实现@Getter注解

lombok中的@Getter注解,可以为自动生成字段的getter方法

模仿ombok的@Getter注解,动手实现@Getter注解

定义注解

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

自定义注解处理器,这里只展示process方法

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
    messager.printMessage(Diagnostic.Kind.NOTE, GetterProcessor.class.getSimpleName() + " round " + (++round));
    for (TypeElement annotation : annotations) {
        roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
            // 获取对应的语法树
            JCTree jcTree = trees.getTree(element);
            // 创建JCClassDecl的visitor,获取字段并创建getter方法
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    super.visitClassDef(jcClassDecl);

                    // 获取变量
                    List jcVariableDecls = List.nil();
                    for (JCTree item : jcClassDecl.defs) {
                        if (item.getKind() == Tree.Kind.VARIABLE) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) item;
                            jcVariableDecls = jcVariableDecls.append(jcVariableDecl);
                        }
                    }

                    // 创建对应的getter方法
                    jcVariableDecls.forEach(jcVariableDecl -> {
                        // 创建getter方法
                        JCTree.JCMethodDecl jcMethodDecl = generateGetterMethod(jcVariableDecl);
                        // 更新类的定义
                        jcClassDecl.defs = jcClassDecl.defs.append(jcMethodDecl);
                    });

                    // 更新jcClassDecl
                    this.result = jcClassDecl;
                }
            });
        });
    }
    return false;
}


private JCTree.JCMethodDecl generateGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
    // 构建方法体中的statement,然后创建方法体
    ListBuffer statements = new ListBuffer<>();
    JCTree.JCReturn jcReturn = treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()));
    statements.add(jcReturn);
    JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
    // 创建JCMethodDecl节点
    Name methodName = getterMethodName(jcVariableDecl.getName()); // 根据字段名生成getter方法名
    JCTree.JCexpression returnType = jcVariableDecl.vartype;
    // 指定方法的修饰符、方法名、返回参数、泛型参数、入参、异常声明、方法体、defaultValue(null即可)
    JCTree.JCMethodDecl jcMethodDecl = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), methodName, returnType, List.nil(), List.nil(), List.nil(), body, null);

    return jcMethodDecl;
}


public Name getterMethodName(Name variableName) {
    String name = variableName.toString();
    return names.fromString("get" + name.substring(0, 1).toUpperCase() + name.substring(1));
}

最好的参考链接:Lombok原理分析与功能实现

4.2 实现setter方法

除了@Getter注解,lombok还有@Setter注解,可以为非final字段生成setter方法

自定义@Setter注解:

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

实现SetterProcessor,只展示process()方法

// 定义elementUtils字段,并在init()方法中初始化
private JavacElements elementUtils;
this.elementUtils = (JavacElements) processingEnv.getElementUtils(); 

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
    messager.printMessage(Diagnostic.Kind.NOTE, SetterProcessor.class.getSimpleName() + " round " + (++round));

    for (TypeElement annotation : annotations) {
        roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
            // 获取对应的语法树
            JCTree jcTree = trees.getTree(element);
            // 通过visitor模式,添加setter方法;如果为final字段,则不生成setter方法
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    super.visitClassDef(jcClassDecl);
                    // 1.获取非final字段
                    List jcVariableDecls = List.nil();
                    for (JCTree item : jcClassDecl.defs) {
                        if (item.getKind() == Tree.Kind.VARIABLE) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) item;
                            if (!jcVariableDecl.getModifiers().getFlags().contains((Modifier.FINAL))) {
                                jcVariableDecls = jcVariableDecls.append(jcVariableDecl);
                            }
                        }
                    }
                    // 2.创建对应的setter方法
                    jcVariableDecls.forEach(jcVariableDecl -> {
                        // 创建对应的setter方法
                        JCTree.JCMethodDecl jcMethodDecl = generateSetterMethod(jcVariableDecl);
                        // 更新类
                        jcClassDecl.defs = jcClassDecl.defs.append(jcMethodDecl);
                    });
                    // 3.更新jcClassDecl
                    this.result = jcClassDecl;
                }
            });
        });
    }

    return roundEnv.processingOver();
}


private JCTree.JCMethodDecl generateSetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
	 // 1.创建赋值语句, 构建方法体
    ListBuffer statements = new ListBuffer<>();
    JCTree.JCexpressionStatement statement = treeMaker.Exec(treeMaker.Assign(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName())));
    statements.append(statement);
    JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

    // 2.生成方法参数之前,指明当前语法节点在语法树中的位置,避免出现异常 java.lang.AssertionError: Value of x -1
    treeMaker.pos = jcVariableDecl.pos;

    // 3.创建方法
    Name methodName = setterMethodName(jcVariableDecl.getName());
    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
     // 通过这种方式定义入参,可能会出现NullPointer错误
    // JCTree.JCVariableDecl param = treeMaker.Param( jcVariableDecl.getName(), jcVariableDecl.vartype.type, jcVariableDecl.sym);
    // 两种定义void返回值的方法等价
    JCTree.JCexpression returnType = treeMaker.Type(new Type.JCVoidType());
    // JCTree.JCexpression returnType = treeMaker.TypeIdent(TypeTag.VOID);
    // 指定方法的修饰符、方法名、返回参数、泛型参数、入参、异常声明、方法体、defaultValue(null即可)
    JCTree.JCMethodDecl jcMethodDecl = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), methodName, returnType, List.nil(), List.of(param), List.nil(), body, null);
   
    return jcMethodDecl;
}


private Name setterMethodName(Name variableName) {
    String name = variableName.toString();
    return names.fromString("set" + name.substring(0, 1).toUpperCase() + name.substring(1));
}

参考链接:Lombok 原理与实现

4.3 自定义@Hello注解

@Hello注解作用于方法,可以让方法在执行代码前先打印类名和方法名,类似:Hello, this is xxx

process()方法定义如下:

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
    messager.printMessage(Diagnostic.Kind.NOTE, HelloProcessor.class.getSimpleName() + " round " + (++round));
    for (TypeElement annotation : annotations) {
        roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
            // 获取方法节点
            JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) trees.getTree(element);
            // 获取方法名,构建需要打印的内容
            String methodName = jcMethodDecl.getName().toString();
            String className = element.getEnclosingElement().getSimpleName().toString();
            String content = String.format("Hello, this is %s() in %s", methodName, className);
            // pos的作用无法体会
            treeMaker.pos = jcMethodDecl.pos;

            // 构建System.out.println语句
            JCTree.JCexpressionStatement printStatement = treeMaker.Exec( // 创建可执行语句
                    treeMaker.Apply( // 创建JCMethodInvocation
                            List.nil(),
                            treeMaker.Select(
                                    treeMaker.Select(treeMaker.Ident(elementUtils.getName("System")), elementUtils.getName("out")), // 第一次select,定位到System.out
                                    elementUtils.getName("println")), // 第二次select,定义到System.out.println
                            List.of(treeMaker.Literal(content))));

            // 更新方法体
            jcMethodDecl.body = treeMaker.Block(0, jcMethodDecl.body.getStatements().prepend(printStatement));
        });
    }

    return false;
}

更新效果:

// 原始的main方法
@Hello
public static void main(String[] args) {
    System.out.println("compile finished");
}
// 更新后,class文件反编译后的main方法
public static void main(String[] args) {
    System.out.println("Hello, this is main() in HelloProcessorTest");
    System.out.println("compile finished");
}

感谢博客:java使用AbstractProcessor、编译时注解和JCTree实现编译时织入代码(类似lombok)并实现Debug自己的Processor和编译后的代码

5. 其他

如何获取代表整个.java文件的JCCompilationUnit:关于ast抽象语法树Jcimport和JCCompilationUnit的用法以公司真实的案例进行讲解,还给出很多示例:java注解处理器——在编译期修改语法树Lombok的介绍与使用lombok的原理(通过修改AST实现):Lombok简介、使用、工作原理、优缺点、Lombok原理【JSR269实战】之编译时操作AST,修改字节码文件,以实现和lombok类似的功能从JSR269到Lombok,学习注解处理器Annotation Processor Tool(附Demo)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/735976.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号