欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

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

时间:2023-06-16
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<? extends TypeElement> 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模式为字段赋默认初始值

@Overridepublic boolean process(Set<? extends TypeElement> 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方法

@Overridepublic boolean process(Set<? extends TypeElement> 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(); @Overridepublic boolean process(Set<? extends TypeElement> 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()方法定义如下:

@Overridepublic boolean process(Set<? extends TypeElement> 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方法@Hellopublic 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)

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。