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源碼: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源碼的實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM