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的方法

//注解处理器会扫描的包名
@SupportedAnnotationTypes("cn.intotw.*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ModCloudAnnotationProcessor extends AbstractProcessor {
private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
Map<String, JCTree.JCAssign> consumerSourceAnnotationValue=new HashMap<>();
Map<String, JCTree.JCAssign> providerSourceAnnotationValue=new HashMap<>();
java.util.List<String> javaBaseVarType;
@Override
public void init(ProcessingEnvironment processingEnv) {
//基本构建,主要是初始化一些操作语法树需要的对象
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = JavacTrees.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()) {
return false;
}
//获取所有增加了自定义注解的element集合
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MobCloudConsumer.class);
//遍历这个集合,这个集合的每个element相当于一个拥有自定义注解的需要处理的类。
set.forEach(element -> {
//获取语法树
JCTree jcTree=trees.getTree(element);
printLog("result :{}",jcTree);
});
return true;
}

上面代码中获取的jctree就是那个class文件解析后的java语法树了,下面来看下有哪些操作。

遍历语法树

java语法树的遍历,并不是能像寻常树节点一样提供child之类的节点,而是通过TreeTranslator这个访问类的实现来做到的,这个类的可供实现的方法有很多,可以用来遍历语法树的注解、方法、变量,基本上语法树的所有java元素,都可以使用这个访问器来访问

//获取源注解的参数
jcTree.accept(new TreeTranslator(){
@Override
public void visitAnnotation(JCTree.JCAnnotation jcAnnotation) {
JCTree.JCIdent jcIdent=(JCTree.JCIdent)jcAnnotation.getAnnotationType();
if(jcIdent.name.contentEquals("MobCloudConsumer")){
printLog("class Annotation arg process:{}",jcAnnotation.toString());
jcAnnotation.args.forEach(e->{
JCTree.JCAssign jcAssign=(JCTree.JCAssign)e ;
JCTree.JCIdent value = treeMaker.Ident(names.fromString("value"));
JCTree.JCAssign targetArg=treeMaker.Assign(value,jcAssign.rhs);
consumerSourceAnnotationValue.put(jcAssign.lhs.toString(),targetArg);
});
}
printLog("获取参数如下:",consumerSourceAnnotationValue);
super.visitAnnotation(jcAnnotation);
}
});

语法树中的源节点

前面说了语法树是有一个个对象组成的,这些对象构成了语法树的一个个源节点,源节点对应java语法中核心的那些语法:

语法树节点类 具体对应的语法元素
JCClassDecl 类的定义
JCMethodDecl 方法的定义
JCAssign 等式(赋值)语句
JCExpression 表达式
JCAnnotation 注解
JCVariableDecl 变量定义

语法树节点的操作

既然说了语法树的那些重要节点,后面直接上案例,该如何操作。需要注意的一点是,Java语法树中所有的操作,对于语法树节点,都不能通过引用操作来复制,必须要从头到尾构造一个一模一样的对象并插入,否则编译是过不去的。

给类增加注解

//该方法最后会给类新增一个@FeignClient(value="")的注解
private void addClassAnnotation(Element element) {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator(){
//遍历所有类定义
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
JCTree.JCExpression arg;
//创建一个value的赋值语句,作为注解的参数
if(jcAssigns==null || jcAssigns.size()==0){
arg=makeArg("value","");
}
printLog("jcAssigns :{}",jcAssigns);
//创建注解对象
JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.FeignClient.toString(),List.of(arg));
printLog("class Annotation add:{}",jcAnnotation.toString());
//在原有类定义中append新的注解对象
jcClassDecl.mods.annotations=jcClassDecl.mods.annotations.append(jcAnnotation);
jcClassDecl.mods.annotations.forEach(e -> {
printLog("class Annotation list:{}",e.toString());
});
super.visitClassDef(jcClassDecl);
}
});
}
public JCTree.JCExpression makeArg(String key,String value){
//注解需要的参数是表达式,这里的实际实现为等式对象,Ident是值,Literal是value,最后结果为a=b
JCTree.JCExpression arg = treeMaker.Assign(treeMaker.Ident(names.fromString(key)), treeMaker.Literal(value));
return arg;
}
private JCTree.JCAnnotation makeAnnotation(String annotaionName, List<JCTree.JCExpression> args){
JCTree.JCExpression expression=chainDots(annotaionName.split("\\."));
JCTree.JCAnnotation jcAnnotation=treeMaker.Annotation(expression, args);
return jcAnnotation;
}

给类增加import语句

private void addImport(Element element,PackageSupportEnum... packageSupportEnums) {
TreePath treePath = trees.getPath(element);
JCTree.JCCompilationUnit jccu = (JCTree.JCCompilationUnit) treePath.getCompilationUnit();
java.util.List<JCTree> trees = new ArrayList<>();
trees.addAll(jccu.defs);
java.util.List<JCTree> sourceImportList = new ArrayList<>();
trees.forEach(e->{
if(e.getKind().equals(Tree.Kind.IMPORT)){
sourceImportList.add(e);
}
});
java.util.List<JCTree.JCImport> needImportList=buildImportList(packageSupportEnums);
for (int i = 0; i < needImportList.size(); i++) {
boolean importExist=false;
for (int j = 0; j < sourceImportList.size(); j++) {
if(sourceImportList.get(j).toString().equals(needImportList.get(i).toString())){
importExist=true;
}
}
if(!importExist){
trees.add(0,needImportList.get(i));
}
}
printLog("import trees{}",trees.toString());
jccu.defs=List.from(trees);
}
private java.util.List<JCTree.JCImport> buildImportList(PackageSupportEnum... packageSupportEnums) {
java.util.List<JCTree.JCImport> targetImportList =new ArrayList<>();
if(packageSupportEnums.length>0){
for (int i = 0; i < packageSupportEnums.length; i++) {
JCTree.JCImport needImport = buildImport(packageSupportEnums[i].getPackageName(),packageSupportEnums[i].getClassName());
targetImportList.add(needImport);
}
}
return targetImportList;
}
private JCTree.JCImport buildImport(String packageName, String className) {
JCTree.JCIdent ident = treeMaker.Ident(names.fromString(packageName));
JCTree.JCImport jcImport = treeMaker.Import(treeMaker.Select(
ident, names.fromString(className)), false);
printLog("add Import:{}",jcImport.toString());
return jcImport;
}

构建一个内部类

这边演示了一个构建内部类的过程,基本就演示了深拷贝一个内部类的过程

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){
//第一个参数加requestbody注解,其他参数加requestparam注解,否则会报错
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);
//value
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);
}
//创建方法调用,如String.format()这种
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文件也打包到对应目录:

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>META-INF/**/*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>process-META</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources/</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

chainDots方法

public  JCTree.JCExpression chainDots(String... elems) {
assert elems != null; JCTree.JCExpression e = null;
for (int i = 0 ; i < elems.length ; i++) {
e = e == null ? treeMaker.Ident(names.fromString(elems[i])) : treeMaker.Select(e, names.fromString(elems[i]));
}
assert e != null; return e;
}

总结

建议这类复杂的语法树操作,不要用来直接在生产进行复杂的扩展,我们本次使用也只是为了快速生成代码。防止手动cp出现失误。原因还是maven构建过程很复杂,哪怕你本地测试通过,真的到了实际项目复杂的构建过程后,不一定能保证代码正确性,甚至会和dubbo以及lombok等组件冲突。

这个技术主要也是以摸索API使用为主,国内没有什么资料,国外的资料也都是语法和类的介绍,实际例子并不多,花了很多时间摸索具体使用的方法,基本能达到实现一切操作了,毕竟注解,方法,类,变量,方法调用,这些都能自定义了,基本也没有什么别的了。期间参考了不少lombok的源码,lombok是在java语法树节点之外利用自己的语法树节点封装了一层,简化和规范了很多操作,可惜我找了一下lombok貌似并没有提供类似于工具包辅助,所以更加深入的使用推荐参考lombok源码的实现。

Java编译期注解处理器详细使用方法的更多相关文章

  1. 手把手教你实现Android编译期注解

    一.编译期注解在开发中的重要性 从早期令人惊艳的ButterKnife,到后来的以ARouter为首的各种路由框架,再到现在谷歌大力推行的Jetpack组件,越来越多的第三方框架都在使用编译期注解这门 ...

  2. java编译期优化

    java语言的编译期其实是一段不确定的操作过程,因为它可以分为三类编译过程: 1.前端编译:把.java文件转变为.class文件 2.后端编译:把字节码转变为机器码 3.静态提前编译:直接把*.ja ...

  3. java编译期优化与执行期优化技术浅析

    java语言的"编译期"是一段不确定的过程.由于它可能指的是前端编译器把java文件转变成class字节码文件的过程,也可能指的是虚拟机后端执行期间编译器(JIT)把字节码转变成机 ...

  4. java 编译期常量

    今天在看书的时候遇到了一个不是很懂的名词,是在think in java 这本书的第七章讲final关键字时讲到的.然后自己在网上查了一下知道了一些. 编译器常量就是:它的值在编译期就可以确定的常量. ...

  5. Java编译期与运行期

    编译期:是指把源码交给编译器编译成计算机可以执行的文件的过程.在Java中也就是把Java代码编成class文件的过程.编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本 ...

  6. java 编译时注解框架 lombok-ex

    lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 编译时注,拥有运行时注解的便利性,和无任何损失的性能. 主要补充一些 lombok 没有实现,且自己会用到的常见 ...

  7. Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)

    注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理.在编译期处理注解时,会处理到不再产生新的源文件为止,之后再对所有源文件进行编译. Java5中提供了apt工具来进行编译期的注解处 ...

  8. Java注解处理器(转)

    Java中的注解(Annotation)是一个很神奇的东西,特别现在有很多Android库都是使用注解的方式来实现的.一直想详细了解一下其中的原理.很有幸阅读到一篇详细解释编写注解处理器的文章.本文的 ...

  9. Java注解处理器使用详解

    在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor).在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么:然后,我将一步一步实 ...

随机推荐

  1. Jmeter之登录接口参数化实战

    为了纪念我走过的坑(为什么有些简单的问题就是绊住我了,还是不够细啊) Jmeter之接口登录参数化实战 因为想要在登录时使用不同的数据进行测试,所以我选择了将数据进行参数化.因为涉及到新建一个接口的功 ...

  2. day109:MoFang:好友列表显示&添加好友页面初始化&添加好友后端接口

    目录 1.好友列表 2.添加好友-前端 3.服务端提供添加好友的后端接口 1.好友列表 1.在用户中心页面添加好友列表点击入口 html/user.html,用户中心添加好友列表点击入口,代码: &l ...

  3. Hibernate实现对数据的CRUD

    今天主要去看公司的老框架, CRUD用的较多,所以总结一下步骤,以免忘记的时候温习 回顾 JDBC 工作过程: 加载驱动 建立连接 定义sql,发生sql语句 执行sql语句获得执行结果 处理返回结果 ...

  4. MyBatis 与 Spring 的完美整合方法

    MyBaits 整合 Spring MyBatis-Spring 项目 第一步:创建测试工程 第二步:引入依赖 jar 包 第三步:编写 Spring 配置文件 第四步:编写 MyBatis 配置文件 ...

  5. linux重启后nginx服务无法启动

    查看ngin.conf pid的内容 例如: pid /usr/local/nginx/logs/nginx.pid 根据以上配置内容来做,检查/usr/local/nginx/logs/是否存在,如 ...

  6. jxl导出Excel中需要跳过的的坑

    正如上篇文章(JXL封装不能使用static关键字问题)所说,在jxl导出Excel时候如果频繁使用到WritableCellFormat去设置单元格的样式,这个时候经常会出现两种情况: 1.报警告: ...

  7. Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

    Spring Cloud 2020.0.0 没错,Spring Cloud 2020.0.0 正式发布了: 感谢Java技术栈群友通知,想入群的在公众号Java技术栈后台回复:wx,正在使用 Spri ...

  8. python初学者-判断今天是今年的第几天代码

    判断今天是今年的第几天源代码 import time date =time.localtime() year,month,day=date[:3] day_month=[31,28,31,30,31, ...

  9. [LeetCode]319. Bulb Switcher灯泡开关

    智商压制的一道题 这个题有个数学定理: 一般数(非完全平方数)的因子有偶数个 完全平凡数的因子有奇数个 开开关的时候,第i个灯每到它的因子一轮的时候就会拨动一下,也就是每个灯拨动的次数是它的因子数 而 ...

  10. 对象存储Backblaze B2作为ShareX图床的Windows及安卓端配置

    标题: 对象存储Backblaze B2作为ShareX图床的Windows及安卓端配置 作者: 梦幻之心星 sky-seeker@qq.com 标签: [对象存储,图床,Backblaze,Shar ...