目录
一、Java编译期注解VS运行期注解
编译期注解 | 运行时注解 | |
---|---|---|
优点 | 模版化代码,代码更简洁,提高开发效率,性能较高 | 可读性强,灵活,易调试 |
缺点 | 代码侵入性强,调试困难,可维护性差 | 性能较差 |
二、Javac生成.class文件流程
- 准备过程:初始化插入式注解处理器。
- 解析与填充符号表过程,包括:
·词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
·填充符号表。产生符号地址和符号信息。 - 插入式注解处理器的注解处理。
- 分析与字节码生成过程,包括:
·标注检查。对语法的静态信息进行检查。
·数据流及控制流分析。对程序动态运行过程进行检查。
·解语法糖。将简化代码编写的语法糖还原为原有的形式。
·字节码生成。将前面各个步骤所生成的信息转化成字节码。
com.sun.tools.javac.main.JavaCompiler
中的核心编译代码
参考《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) - 周志明》
三、插件化注解API(Pluggable Annotation Processing API)
1、概念
按照 JSR269标准提供的一种API来操作annotation,它旨在通过编译期注解模版化地生成代码。
可以把插入式注解处理器看作是一组编译器的插件,如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
2、使用步骤
- 自定义一个元注解为
@Retention(RetentionPolicy.SOURCE)
的注解 - 自定义一个继承至
javax.annotation.processing.AbstractProcessor
的Processor来处理注解 - 用自定义的Processor来编译Java文件,并指定其所支持的注解
- 将自定义注解标注在自己所需要的地方!
3、关键类
JCTree.java
语法树节点的基类
TreeMaker.java
用于构建语法树节点
TreeTranslator.java
继承自JCTree.Vistor类,可以访问所有语法树节点,并对其进行代码侵入
4、案例
Talk is cheap. Show me the code
- 这里以一个多模块maven项目为例,项目结构如下,启动类为
DemoApplication.java
。
这里的案例为做这件事:将HTTP请求和返回的空字符串统一置为null
- 仅在编译期生效的元注解
package com.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface RewriteEmptyStringAsNull {}
- 自定义Processor,
/*
*MyProcessor.java
*/
package com.demo.processor;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import com.google.auto.service.AutoService;
import com.sun.source.util.Trees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Names;
// Google提供的注解,用于注册Java SPI
@AutoService(Processor.class)
// Processor对哪些注解生效
@SupportedAnnotationTypes({"com.demo.annotation.*"})
public class MyProcessor extends AbstractProcessor {
/**
* 语法树集合
*/
private Trees trees;
/*
* 修改语法树的工具类
*/
private TreeMaker treeMaker;
/**
* 给语法树元素命名的工具类
*/
private Names names;
/**
* 用来打印错误信息
*/
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = Trees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
// 遍历每个被注解标记的元素
for (Element element : roundEnv.getRootElements()) {
if (element.getKind().isClass()) {
// 获取语法树
JCTree tree = (JCTree) trees.getTree(element);
// 使用TreeTranslator遍历
tree.accept(new MyTreeTranslator(treeMaker, names));
}
}
}
// 是否继续让其他processor处理
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
/*
* MyTreeTranslator.java
*/
package com.demo.processor;
import static com.sun.tools.javac.tree.JCTree.JCAnnotation;
import static com.sun.tools.javac.tree.JCTree.JCBlock;
import static com.sun.tools.javac.tree.JCTree.JCModifiers;
import static com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Names;
import com.demo.annotation.RewriteEmptyStringAsNull;
public class MyTreeTranslator extends TreeTranslator {
/**
* 树节点创建工具类
*/
private TreeMaker treeMaker;
/**
* 命名工具类
*/
private Names names;
/**
* 需要插入的Setter方法
*/
private List<JCTree> setters = List.nil();
public MyTreeTranslator(TreeMaker treeMaker, Names names) {
this.treeMaker = treeMaker;
this.names = names;
}
/**
* 遍历到类时执行
*/
@Override
public void visitClassDef(JCClassDecl jcClassDecl) {
super.visitClassDef(jcClassDecl);
// 插入setter方法
if (!setters.isEmpty()) {
jcClassDecl.defs = jcClassDecl.defs.appendList(this.setters);
}
this.result = jcClassDecl;
}
/**
* 遍历成员时执行
*/
@Override
public void visitVarDef(JCVariableDecl jcVariableDecl) {
super.visitVarDef(jcVariableDecl);
JCModifiers modifiers = jcVariableDecl.getModifiers();
List<JCAnnotation> annotations = modifiers.getAnnotations();
if (annotations == null || annotations.size() <= 0) {
return;
}
// 以下只是简单实现, 不考虑是否已存在setter方法
for (JCAnnotation annotation : annotations) {
if (RewriteEmptyStringAsNull.class.getName().equals(annotation.type.toString())) {
// 生成getter方法
JCMethodDecl setterMethod = createSetterMethod(jcVariableDecl);
this.setters = this.setters.append(setterMethod);
}
}
}
/**
* 创建Setter方法
*/
private JCMethodDecl createSetterMethod(JCVariableDecl field) {
// 定义方法传入的参数
JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), field.name, field.vartype, null);
/*
if("".equals(name)){
this.name = null;
}else{
this.name = name;
}
*/
// "".equals(name) ---方法调用节点
JCTree.JCMethodInvocation judgeStat = treeMaker.Apply(
// 参数类型列表
List.nil(),
// "".equals ---调用语句
treeMaker.Select(
// . 左边的内容, 被调用的实体
treeMaker.Literal(""),
// . 右边的内容, 调用方法名/变量名等
names.fromString("equals")
),
// name ---方法调用的传参
List.of(treeMaker.Ident(field.name))
);
// this.name = null --- 可执行语句节点
JCTree.JCExpressionStatement ifStat = treeMaker.Exec(
// 赋值语句节点
treeMaker.Assign(
// = 左边的语句
treeMaker.Select(treeMaker.Ident(names.fromString("this")), field.name),
// = 右边的语句
treeMaker.Literal(TypeTag.BOT,null)
)
);
// this.name = name --- 可执行语句节点
JCTree.JCExpressionStatement elseStat = treeMaker.Exec(
// 赋值语句节点
treeMaker.Assign(
// = 左边的语句
treeMaker.Select(treeMaker.Ident(names.fromString("this")), field.name),
// = 右边的语句
treeMaker.Ident(field.name)
)
);
JCBlock methodBody = treeMaker.Block(0L, List.of(treeMaker.If(
// 判断语句
judgeStat,
// 条件成立的语句
ifStat,
// 条件不成立的语句
elseStat
)));
return treeMaker.MethodDef(
// public方法
treeMaker.Modifiers(Flags.PUBLIC),
// 方法名称
names.fromString("set" + this.toTitleCase(field.getName().toString())),
// 方法返回的类型
treeMaker.Type(new Type.JCVoidType()),
// 泛型参数
List.nil(),
// 方法参数
List.of(param),
// throw表达式
List.nil(),
// 方法体
methodBody,
// 默认值
null
);
}
/**
* 首字母大写
*/
public String toTitleCase(String str) {
char first = str.charAt(0);
if (first >= 'a' && first <= 'z') {
first -= 32;
}
return first + str.substring(1);
}
}
- 用自定义Processor来编译java文件
推荐使用Java SPI来注册指定Processor,有两种方式
- 在自定义的Processor上添加注解`@AutoService({Processor.class})
- 新增文件src/main/resources/META-INF/services/javax.annotation.processing.Processor 内容为自定义Processor的全限定名,如
com.demo.processor.MyProcessor
- 效果演示
/*
* HelloWorldController.java
*/
package com.demo.controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.demo.vo.Hello1Req;
import com.demo.vo.Hello2Req;
@RestController
public class HelloWorldController {
@RequestMapping("/hello1")
public Object hello1(@RequestBody Hello1Req hello1Req) {
return hello1Req;
}
@RequestMapping("/hello2")
public Object hello2(@RequestBody Hello2Req hello2Req) {
return hello2Req;
}
}
/*
* Hello1Req.java
*/
package com.demo.vo;
import lombok.Data;
@Data
public class Hello1Req {
private String str1;
private String str2;
}
/*
* Hello2Req.java
*/
package com.demo.vo;
import com.demo.annotation.RewriteEmptyStringAsNull;
import lombok.Getter;
public class Hello2Req {
@Getter
@RewriteEmptyStringAsNull
private String str1;
@Getter
@RewriteEmptyStringAsNull
private String str2;
}
4.1 Rebuild Project 查看class文件
成功生成自定义方法
4.2 接口调用,查看是否成功实现:将HTTP请求和返回的空字符串统一置为null
启动springboot项目,分别调用这两个方法
Request1
curl -X POST localhost:8080/hello1 \
--header 'Content-Type: application/json' \
--data '{
"str1":"哈哈",
"str2":""
}'
Request2
curl -X POST localhost:8080/hello2 \
--header 'Content-Type: application/json' \
--data '{
"str1":"哈哈",
"str2":""
}'
结果
5、Tips
- 如何debug自定义的processor?
打好断点后,通过调试javac源码,即debug如下的main方法
public static void main(String[] args)throws Exception {
com.sun.tools.javac.Main.main(new String[] {"-proc:only",
"-processor",
// processor全限定名
"com.demo.processor.MyProcessor",
// 需要编译的文件路径
"/Users/Project/test/demo-main/src/main/java/com/demo/vo/Hello2Req.java"});
// SpringApplication.run(DemoApplication.class, args);
}
- 如何规避IDEA的编译期语法检查?
在Preferences | Build, Execution, Deployment | Compiler
的Shared build process VM options 添加
-Djps.track.ap.dependencies=false
- 自定义Processor必须在注解标注的类之前编译,换句话说,注解标注的类必须经过Processor进行编译。因此建议把Processor写在另一个模块,通过maven引用此依赖来强制指定编译顺序。