一开始学习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
最好的参考链接: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
参考链接: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)