Java编译期注解处理器
Java编译期注解处理器,Annotation Processing Tool,简称APT,是Java提供给开发者的用于在编译期对注解进行处理的一系列API,这类API的使用被广泛的用于各种框架,如dubbo,lombok等。
Java的注解处理一般分为2种,最常见也是最显式化的就是Spring以及Spring Boot的注解实现了,在运行期容器启动时,根据注解扫描类,并加载到Spring容器中。而另一种就是本文主要介绍的注解处理,即编译期注解处理器,用于在编译期通过JDK提供的API,对Java文件编译前生成的Java语法树进行处理,实现想要的功能。
前段公司要求将原有dubbo迁入spring cloud架构,理所当然的最简单的方式,就是将原有的dubboRpc服务类,外面封装一层controller,并且将调用改成feignClient,这样能短时间的兼容原有其他未升级云模块的dubbo调用,之前考虑过其他方案,比如spring cloud sidecar。但是运维组反对,不建议每台机器多加一个服务,并且只是为了短时间过渡,没必要多加一个技术栈,所以考虑使用编译期处理器来快速生成类似的java代码,避免手动大量处理会产生操作失误。
练手项目示例的git源码:https://github.com/IntoTw/mob
启用注解处理器
增加这么一个类,实现AbstractProcessor的方法
上面代码中获取的jctree就是那个class文件解析后的java语法树了,下面来看下有哪些操作。
遍历语法树
java语法树的遍历,并不是能像寻常树节点一样提供child之类的节点,而是通过TreeTranslator这个访问类的实现来做到的,这个类的可供实现的方法有很多,可以用来遍历语法树的注解、方法、变量,基本上语法树的所有java元素,都可以使用这个访问器来访问
语法树中的源节点
前面说了语法树是有一个个对象组成的,这些对象构成了语法树的一个个源节点,源节点对应java语法中核心的那些语法:
语法树节点类 |
具体对应的语法元素 |
JCClassDecl |
类的定义 |
JCMethodDecl |
方法的定义 |
JCAssign |
等式(赋值)语句 |
JCExpression |
表达式 |
JCAnnotation |
注解 |
JCVariableDecl |
变量定义 |
语法树节点的操作
既然说了语法树的那些重要节点,后面直接上案例,该如何操作。需要注意的一点是,Java语法树中所有的操作,对于语法树节点,都不能通过引用操作来复制,必须要从头到尾构造一个一模一样的对象并插入,否则编译是过不去的。
给类增加注解
给类增加import语句
构建一个内部类
这边演示了一个构建内部类的过程,基本就演示了深拷贝一个内部类的过程
private JCTree.JCClassDecl buildInnerClass(JCTree.JCClassDecl sourceClassDecl, java.util.List<JCTree.JCMethodDecl> methodDecls) {
java.util.List<JCTree.JCVariableDecl> jcVariableDeclList = buildInnerClassVar(sourceClassDecl);
String lowerClassName=sourceClassDecl.getSimpleName().toString();
lowerClassName=lowerClassName.substring(0,1).toLowerCase().concat(lowerClassName.substring(1));
java.util.List<JCTree.JCMethodDecl> jcMethodDecls = buildInnerClassMethods(methodDecls,
lowerClassName);
java.util.List<JCTree> jcTrees=new ArrayList<>();
jcTrees.addAll(jcVariableDeclList);
jcTrees.addAll(jcMethodDecls);
JCTree.JCClassDecl targetClassDecl = treeMaker.ClassDef(
buildInnerClassAnnotation(),
names.fromString(sourceClassDecl.name.toString().concat("InnerController")),
List.nil(),
null,
List.nil(),
List.from(jcTrees));
return targetClassDecl;
}
private java.util.List<JCTree.JCVariableDecl> buildInnerClassVar(JCTree.JCClassDecl jcClassDecl) {
String parentClassName=jcClassDecl.getSimpleName().toString();
printLog("simpleClassName:{}",parentClassName);
java.util.List<JCTree.JCVariableDecl> jcVariableDeclList=new ArrayList<>();
java.util.List<JCTree.JCAnnotation> jcAnnotations=new ArrayList<>();
JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.Autowired.toString()
,List.nil());
jcAnnotations.add(jcAnnotation);
JCTree.JCVariableDecl jcVariableDecl = treeMaker.VarDef(treeMaker.Modifiers(1, from(jcAnnotations)),
names.fromString(parentClassName.substring(0, 1).toLowerCase().concat(parentClassName.substring(1))),
treeMaker.Ident(names.fromString(parentClassName)),
null);
jcVariableDeclList.add(jcVariableDecl);
return jcVariableDeclList;
}
private JCTree.JCModifiers buildInnerClassAnnotation() {
JCTree.JCExpression jcAssign=makeArg("value",providerSourceAnnotationValue.get("feignClientPrefix").rhs.toString().replace("\"",""));
JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.RequestMapping.toString(),
List.of(jcAssign)
);
JCTree.JCAnnotation restController=makeAnnotation(PackageSupportEnum.RestController.toString(),List.nil());
JCTree.JCModifiers mods=treeMaker.Modifiers(Flags.PUBLIC|Flags.STATIC,List.of(jcAnnotation).append(restController));
return mods;
}
private java.util.List<JCTree.JCMethodDecl> buildInnerClassMethods(java.util.List<JCTree.JCMethodDecl> methodDecls,String serviceName) {
java.util.List<JCTree.JCMethodDecl> target = new ArrayList<>();
methodDecls.forEach(e -> {
if (!e.name.contentEquals("<init>")) {
java.util.List<JCTree.JCVariableDecl> targetParams=new ArrayList<>();
e.params.forEach(param->{
JCTree.JCVariableDecl newParam=treeMaker.VarDef(
(JCTree.JCModifiers) param.mods.clone(),
param.name,
param.vartype,
param.init
);
printLog("copy of param:{}",newParam);
targetParams.add(newParam);
});
JCTree.JCMethodDecl methodDecl = treeMaker.MethodDef(
(JCTree.JCModifiers) e.mods.clone(),
e.name,
(JCTree.JCExpression) e.restype.clone(),
e.typarams,
e.recvparam,
List.from(targetParams),
e.thrown,
treeMaker.Block(0L,List.nil()),
e.defaultValue
);
target.add(methodDecl);
}
});
target.forEach(e -> {
if (e.params.size() > 0) {
for (int i = 0; i < e.params.size() ; i++) {
JCTree.JCVariableDecl jcVariableDecl=e.params.get(i);
if(i==0){
if(!isBaseVarType(jcVariableDecl.vartype.toString()))
{
jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(makeAnnotation(PackageSupportEnum.RequestBody.toString(), List.nil()));
}else {
JCTree.JCAnnotation requestParam=makeAnnotation(PackageSupportEnum.RequestParam.toString(),
List.of(makeArg("value",jcVariableDecl.name.toString())));
jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(requestParam);
}
}else {
JCTree.JCAnnotation requestParam=makeAnnotation(PackageSupportEnum.RequestParam.toString(),
List.of(makeArg("value",jcVariableDecl.name.toString())));
jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(requestParam);
}
}
}
printLog("sourceMethods: {}", e);
JCTree.JCExpression jcAssign=makeArg("value","/"+e.name.toString());
JCTree.JCAnnotation jcAnnotation = makeAnnotation(
PackageSupportEnum.PostMapping.toString(),
List.of(jcAssign)
);
printLog("annotation: {}", jcAnnotation);
e.mods.annotations = e.mods.annotations.append(jcAnnotation);
JCTree.JCExpressionStatement exec = getMethodInvocationStat(serviceName, e.name.toString(), e.params);
if(!e.restype.toString().contains("void")){
JCTree.JCReturn jcReturn=treeMaker.Return(exec.getExpression());
e.body.stats = e.body.stats.append(jcReturn);
}else {
e.body.stats = e.body.stats.append(exec);
}
});
return List.from(target);
}
private JCTree.JCExpressionStatement getMethodInvocationStat(String invokeFrom, String invokeMethod, List<JCTree.JCVariableDecl> args) {
java.util.List<JCTree.JCIdent> params = new ArrayList<>();
args.forEach(e -> {
params.add(treeMaker.Ident(e.name));
});
JCTree.JCIdent invocationFrom = treeMaker.Ident(names.fromString(invokeFrom));
JCTree.JCFieldAccess jcFieldAccess1 = treeMaker.Select(invocationFrom, names.fromString(invokeMethod));
JCTree.JCMethodInvocation apply = treeMaker.Apply(nil(), jcFieldAccess1, List.from(params));
JCTree.JCExpressionStatement exec = treeMaker.Exec(apply);
printLog("method invoke:{}", exec);
return exec;
}
使用方法
注解器的实际使用,需要在resource文件夹下的META-INF.services文件夹下,新建一个叫做javax.annotation.processing.Processor的文件,里面写上需要生效的类注解处理器的包名加类名,例如:cn.intotw.mob.ModCloudAnnotationProcessor。
然后如果是作为第三方jar包提供给别人,需要在maven打包时增加如下配置,主要也是把javax.annotation.processing.Processor文件也打包到对应目录:
chainDots方法
总结
建议这类复杂的语法树操作,不要用来直接在生产进行复杂的扩展,我们本次使用也只是为了快速生成代码。防止手动cp出现失误。原因还是maven构建过程很复杂,哪怕你本地测试通过,真的到了实际项目复杂的构建过程后,不一定能保证代码正确性,甚至会和dubbo以及lombok等组件冲突。
这个技术主要也是以摸索API使用为主,国内没有什么资料,国外的资料也都是语法和类的介绍,实际例子并不多,花了很多时间摸索具体使用的方法,基本能达到实现一切操作了,毕竟注解,方法,类,变量,方法调用,这些都能自定义了,基本也没有什么别的了。期间参考了不少lombok的源码,lombok是在java语法树节点之外利用自己的语法树节点封装了一层,简化和规范了很多操作,可惜我找了一下lombok貌似并没有提供类似于工具包辅助,所以更加深入的使用推荐参考lombok源码的实现。
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~