Java编译期注解处理器详细使⽤⽅法
⽬录
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源码:
启⽤注解处理器
增加这么⼀个类,实现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);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
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 = ElementsAnnotatedWith(MobCloudConsumer.class);
//遍历这个集合,这个集合的每个element相当于⼀个拥有⾃定义注解的需要处理的类。
set.forEach(element -> {
/
/获取语法树
JCTree Tree(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.AnnotationType();
if(tEquals("MobCloudConsumer")){
printLog("class Annotation arg process:{}",String());
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(String(),targetArg);
});
}
printLog("获取参数如下:",consumerSourceAnnotationValue);
super.visitAnnotation(jcAnnotation);
}
});
前⾯说了语法树是有⼀个个对象组成的,这些对象构成了语法树的⼀个个源节点,源节点对应java语法中核⼼的那些语法:
语法树节点类具体对应的语法元素
JCClassDecl类的定义
JCMethodDecl⽅法的定义
JCAssign等式(赋值)语句
JCExpression表达式
JCAnnotation注解
JCVariableDecl变量定义
语法树节点的操作
既然说了语法树的那些重要节点,后⾯直接上案例,该如何操作。需要注意的⼀点是,Java语法树中所有的操作,对于语法树节点,都不能通过引⽤操作来复制,必须要从头到尾构造⼀个⼀模⼀样的对象并插⼊,否则编译是过不去的。
给类增加注解
//该⽅法最后会给类新增⼀个@FeignClient(value="")的注解
private void addClassAnnotation(Element element) {
JCTree jcTree = Tree(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(String(),List.of(arg));
printLog("class Annotation add:{}",String());
//在原有类定义中append新的注解对象
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 packageSupportEnums) {
TreePath treePath = Path(element);
JCTree.JCCompilationUnit jccu = (JCTree.JCCompilationUnit) CompilationUnit();
java.util.List<JCTree> trees = new ArrayList<>();
trees.addAll(jccu.defs);
java.util.List<JCTree> sourceImportList = new ArrayList<>();
trees.forEach(e->{
Kind().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++) {
(j).toString().(i).toString())){
importExist=true;
}
}
if(!importExist){
trees.add((i));
}
jccu.defs=List.from(trees);
}
private java.util.List<JCTree.JCImport> 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:{}",String());
return jcImport;
}
构建⼀个内部类
这边演⽰了⼀个构建内部类的过程,基本就演⽰了深拷贝⼀个内部类的过程
private JCTree.JCClassDecl buildInnerClass(JCTree.JCClassDecl sourceClassDecl, java.util.List<JCTree.JCMethodDecl> methodDecls) { java.util.List<JCTree.JCVariableDecl> jcVariableDeclList = buildInnerClassVar(sourceClassDecl);java dubbo
String SimpleName().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(String().concat("InnerController")),
List.nil(),
null,
List.nil(),
List.from(jcTrees));
return targetClassDecl;
}
private java.util.List<JCTree.JCVariableDecl> buildInnerClassVar(JCTree.JCClassDecl jcClassDecl) {
String SimpleName().toString();
printLog("simpleClassName:{}",parentClassName);
java.util.List<JCTree.JCVariableDecl> jcVariableDeclList=new ArrayList<>();
java.util.List<JCTree.JCAnnotation> jcAnnotations=new ArrayList<>();
JCTree.JCAnnotation jcAnnotation=makeAnnotation(String()
,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",("feignClientPrefix").String().replace("\"",""));
JCTree.JCAnnotation jcAnnotation=makeAnnotation(String(),
List.of(jcAssign)
);
JCTree.JCAnnotation restController=makeAnnotation(String(),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 (!tEquals("<init>")) {
java.util.List<JCTree.JCVariableDecl> targetParams=new ArrayList<>();
e.params.forEach(param->{
JCTree.JCVariableDecl newParam=treeMaker.VarDef(
(JCTree.JCModifiers) ds.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.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=(i);
if(i==0){
/
/第⼀个参数加requestbody注解,其他参数加requestparam注解,否则会报错
if(!isBaseVarType(String()))
{
}else {
JCTree.JCAnnotation requestParam=makeAnnotation(String(),
List.of(makeArg("value",String())));
}
}else {
JCTree.JCAnnotation requestParam=makeAnnotation(String(),
List.of(makeArg("value",String())));
}
}
}
printLog("sourceMethods: {}", e);
//value
JCTree.JCExpression jcAssign=makeArg("value","/"+String());
JCTree.JCAnnotation jcAnnotation = makeAnnotation(
String(),
List.of(jcAssign)
);
printLog("annotation: {}", jcAnnotation);
JCTree.JCExpressionStatement exec = getMethodInvocationStat(serviceName, String(), e.params);
if(!String().contains("void")){
JCTree.JCReturn jcReturn=treeMaker.Expression());
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的⽂件,⾥⾯写上需要⽣效的类注解处理器的包名加类名,例如:b.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>
<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 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源码的实现。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论