一开始学习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(); // 获取value3. 代码实战 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模式为字段赋默认初始值
@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();
// 通过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 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();
@Override
public 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()方法定义如下:
@Override
public 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方法
@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)



