JAVA 插入注解處理器


JDK1.5后,Java語言提供了對注解(Annotation)的支持

JDK1.6中提供一組插件式注解處理器的標准API,可以實現API自定義注解處理器,干涉編譯器的行為。

 

在這里,注解處理器可以看作編譯器的插件,在編譯期間對注解進行處理,可以對語法樹進行讀取、修改、添加任意元素;但如果有注解處理器修改了語法樹,編譯器將返回解析及填充符號表的過程,重新處理,直到沒有注解處理器修改為止,每一次重新處理循環稱為一個Round。

 

平時工作中,使用的注解,除了框架 等自帶的注解,還有一些自定義的注解,而一般情況下使用自定義注解,主要是用來使用AOP對其

進行增強的。

而這篇博客所說的插件式注解處理器,是直接干預生成的字節碼的文件的。

Java常用的Lombok , Android 常用的 ButterKnife 就屬於此類,通過干預 Java的編譯過程來達到代碼增強的著名類庫。

 

先簡單描述一下Java文件的編譯:

Java前端編譯(Java三種編譯方式:前端編譯 JIT編譯 AOT編譯):Java源代碼編譯成Class文件的過程

javac編譯器是官方JDK中提供的前端編譯器,JDK/bin目錄下的javac只是一個與平台相關的調用入口,具體實現在JDK/lib目錄下的tools.jar。此外,JDK6開始提供在運行時進行前端編譯,默認也是調用到javac

javac是由Java語言編寫的,而HotSpot虛擬機則是由C++語言編寫;標准JDK中並沒有提供javac的源碼,而在OpenJDK中的提供

javac編譯器程序入口:com.sun.tools.javac.Main類中的main()方法

 

 

 

我們先來了解下javac的編譯過程,大致可以分為3個過程,分別是:

  1. 解析與填充符號表過程
  2. 插入式注解處理器的注解處理過程(jsr269規范)
  3. 分析與字節碼生成過程

解析與填充符號表過程會將源碼轉換為一棵抽象語法樹(Abstract Syntax Tree,AST),AST是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼注釋等都可以是一個語法結構。

因此可以利用Java的插入式注解處理器提供的API,讀取、修改、添加抽象語法樹中的任意元素。如果因為這些注解對語法樹進行了修改,編譯器會重新進行詞法、語法的分析處理,直到所有的插入式注解沒有對語法樹進行修改為止。

 

那么,就以一個例子來說說這個插入式注解處理器:

平時工作中有時候需要查看一個方法的執行耗時,那么通常的做法是方法體前后通過兩個時間戳變量來計算,這種情況對於單個方法使用,但是多個方法就顯得比較麻煩了。當然也可以通過一些設計模式來解決這個問題

如果恰好使用Spring,那么使用AOP也可以解決這個問題!

但是說來說去,我就是想看一下方法的執行時間,上述的方法都太過麻煩!怎么才能方便的解決這個問題呢?

這里我先自定義一個注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface TakeTime {

    /**
     * 標記前綴,無實質作用,只是為了方便查找
     * @return
     */
    String tag() default "";
}

這是一個非常簡單的自定義注解,注解只能標注在方法上,同時注解的有效期只在@Retention(RetentionPolicy.SOURCE) 源碼期

這里要說明的是,這個有效期在我這個demo中可以是任意值,因為本博客說的插入式注解處理器,不管注解的生命周期是什么值,插入式注解處理器都會進行處理。

如果你的需求是,不但在編譯時你需要這個注解,其他時候也需要該注解,比如我想在運行期攔截這個注解 等等!那么請更改注解的生命周期,已符合對應業務。

而如果僅僅只需要編譯代碼,那么設置為源碼期就足夠了

 

那么有了注解了,接下來就是注解處理器

package cn.kanyun.annotation_processor.taketime;

import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.model.JavacElements;
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.List;
import com.sun.tools.javac.util.Names;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.Set;

/**
 * @TakeTime 注解的注解處理器
 * @SupportedSourceVersion 表示對應的版本
 * @SupportedAnnotationTypes 表示處理哪種類型的注解(這是一個集合, 其值注解的全限定名)
 * @AutoService @AutoService(Processor.class) :向javac注冊我們這個自定義的注解處理器,
 * 這樣,在javac編譯時,才會調用到我們這個自定義的注解處理器方法。@AutoService這里主要是用來生成
 * META-INF/services/javax.annotation.processing.Processor文件的。如果不加上這個注解,那么,你需要自己進行手動配置進行注冊
 * <p>
 * AbstractProcessor是注解處理器的抽象類,我們通過繼承AbstractProcessor類然后實現process方法來創建我們自己的注解處理器,
 * 所有處理注解的代碼放在process方法里面
 */

@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
@SupportedAnnotationTypes(value = {"cn.kanyun.annotation_processor.taketime.TakeTime"})
@AutoService(Processor.class)
public class TakeTimeProcessor extends AbstractProcessor {

    /**
     * Messager接口提供注解處理器用來報告錯誤消息、警告和其他通知的方式
     * 它不是注解處理器開發者的日志工具,而是用來寫一些信息給使用此注解器的第三方開發者的
     * 注意:我們應該對在處理過程中可能發生的異常進行捕獲,通過Messager接口提供的方法通知用戶(在官方文檔中描述了消息的不同級別。非常重要的是Kind.ERROR)。
     * 此外,使用帶有Element參數的方法連接到出錯的元素,
     * 用戶可以直接點擊錯誤信息跳到出錯源文件的相應行。
     * 如果你在process()中拋出一個異常,那么運行注解處理器的JVM將會崩潰(就像其他Java應用一樣),
     * 這樣用戶會從javac中得到一個非常難懂出錯信息
     */
    private Messager messager;

    /**
     * 實現Filer接口的對象,用於創建文件、類和輔助文件。
     * 使用Filer你可以創建文件
     * Filer中提供了一系列方法,可以用來創建class、java、resources文件
     * filer.createClassFile()[創建一個新的類文件,並返回一個對象以允許寫入它]
     * filer.createResource() [創建一個新的源文件,並返回一個對象以允許寫入它]
     * filer.createSourceFile() [創建一個用於寫入操作的新輔助資源文件,並為它返回一個文件對象]
     */
    private Filer filer;

    /**
     * 用來處理Element的工具類
     * Elements接口的對象,用於操作元素的工具類。
     */
    private JavacElements elementUtils;

    /**
     * 用來處理TypeMirror的工具類
     * 實現Types接口的對象,用於操作類型的工具類。
     */
    private Types typeUtils;

    /**
     * 這個依賴需要將${JAVA_HOME}/lib/tools.jar 添加到項目的classpath,IDE默認不加載這個依賴
     */
    private JavacTrees trees;

    /**
     * 這個依賴需要將${JAVA_HOME}/lib/tools.jar 添加到項目的classpath,IDE默認不加載這個依賴
     * TreeMaker創建語法樹節點的所有方法,創建時會為創建出來的JCTree設置pos字段,
     * 所以必須用上下文相關的TreeMaker對象來創建語法樹節點,而不能直接new語法樹節點。
     */
    private TreeMaker treeMaker;

    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        elementUtils = (JavacElements) processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    /**
     * 該方法將一輪一輪的遍歷源代碼
     * 處理注解前需要先獲取兩個重要信息,
     * 第一是注解本身的信息,具體來說就是獲取注解對象,有了注解對象以后就可以獲取注解的值。
     * 第二是被注解元素的信息,具體來說就是獲取被注解的字段、方法、類等元素的信息
     *
     * @param annotations 該方法需要處理的注解類型
     * @param roundEnv    關於一輪遍歷中提供給我們調用的信息.
     * @return 該輪注解是否處理完成 true 下輪或者其他的注解處理器將不會接收到次類型的注解.用處不大.
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//        roundEnv.getRootElements()會返回工程中所有的Class,在實際應用中需要對各個Class先做過濾以提高效率,避免對每個Class的內容都進行掃描
        roundEnv.getRootElements();
        messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解處理器處理中");
        TypeElement currentAnnotation = null;
//        遍歷注解集合,也即@SupportedAnnotationTypes中標注的類型
        for (TypeElement annotation : annotations) {
            messager.printMessage(Diagnostic.Kind.NOTE, "遍歷本注解處理器處理的所有注解,當前遍歷到的注解是:" + annotation.getSimpleName());
            currentAnnotation = annotation;
        }
//      獲取所有包含 TakeTime 注解的元素(roundEnv.getElementsAnnotatedWith(TakeTime.class))返回所有被注解了@Factory的元素的列表。你可能已經注意到,我們並沒有說“所有被注解了@TakeTime的方法的列表”,因為它真的是返回Element的列表。請記住:Element可以是類、方法、變量等。所以,接下來,我們必須檢查這些Element是否是一個方法)
        Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(TakeTime.class);
        messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解處理器處理@TakeTime注解");
        for (Element element : elementSet) {
                //獲取注解
                TakeTime TakeTimeAnnotation = element.getAnnotation(TakeTime.class);
                //獲取注解中配置的值
                String tag = TakeTimeAnnotation.tag();
                messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "注解上設置的值為:" + tag);

//                TypeSpec typeSpec = generateCodeByPoet(typeElement, null);

//                方法名(這里之所以是方法名,是因為這個注解是標注在方法上的)
                String methodName = element.getSimpleName().toString();

//                類名[全限定名]
//                element.getEnclosingElement()返回封裝此元素(非嚴格意義上)的最里層元素,由於我們在上面判斷了element是method類型,所以直接封裝method的的就是類了
//                http://www.169it.com/article/3400309390285698450.html
                String className = element.getEnclosingElement().toString();

                messager.printMessage(Diagnostic.Kind.NOTE, "當前被標注注解的方法所在的類是:" + className);
                messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "當前被標注注解的方法是:" + methodName);

//                JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
                enhanceMethodDecl(elementUtils.getTree(element), tag, className + "." + methodName);



            if (element.getKind() == ElementKind.FIELD) {
//                當前element是字段類型
                VariableElement variableElement = (VariableElement) element;
                messager.printMessage(Diagnostic.Kind.ERROR, "字段不能使用@TakeTime注解", element);
            }

            if (element.getKind() == ElementKind.CONSTRUCTOR) {
//                當前element是構造方法類型

            }
        }


        return false;
    }


    /**
     * 方法增強
     *
     * @param jcTree
     * @param methodName 方法的全限定名
     * @param tag        標識
     * @return
     */
    private JCTree.JCMethodDecl enhanceMethodDecl(JCTree jcTree, String tag, String methodName) {
        JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree;

//        生成表達式System.currentTimeMillis()
        JCTree.JCExpressionStatement time = treeMaker.Exec(treeMaker.Apply(
                //參數類型(傳入方法的參數的類型) 如果是無參的不能設置為null 使用 List.nil()
                List.nil(),
                memberAccess("java.lang.System.currentTimeMillis"),
                //因為不需要傳遞參數,所以直接設置為List.nil() 不能設置為null
                List.nil()
                //參數集合[集合中每一項的類型需要跟第一個參數對照]
//                List.of(treeMaker.Literal())
                )
        );


//        編譯后該方法會存在一個startTime的變量,其值為編譯時的時間
        JCTree.JCVariableDecl startTime = createVarDef(treeMaker.Modifiers(0), "startTime", memberAccess("java.lang.Long"), treeMaker.Literal(System.currentTimeMillis()));

//        耗時計算表示式
        JCTree.JCExpressionStatement timeoutStatement = treeMaker.Exec(
                treeMaker.Apply(
                        List.of(memberAccess("java.lang.Long"), memberAccess("java.lang.Long")),
                        memberAccess("java.lang.Math.subtractExact"),
                        List.of(time.expr, treeMaker.Ident(startTime.name))
                )

        );
//
        messager.printMessage(Diagnostic.Kind.NOTE, "::::::::::::::::::::");
        messager.printMessage(Diagnostic.Kind.NOTE, timeoutStatement.expr.toString());

//        生成表達式System.out.println()
        JCTree.JCExpressionStatement TakeTime = treeMaker.Exec(treeMaker.Apply(
                //參數類型(傳入方法的參數的類型) 如果是無參的不能設置為null 使用 List.nil()
                List.of(memberAccess("java.lang.String"), memberAccess("java.lang.String"), memberAccess("java.lang.Long")),
//                因為這里要傳多個參數,所以此處應使用printf,而不是println
                memberAccess("java.lang.System.out.printf"),
                //取到前面定義的startTime的變量
//                List.of(treeMaker.Ident(startTime.name))
//                取得結果
                List.of(treeMaker.Literal(">>>>>>>>TAG:%s -> 方法%s執行用時:%d<<<<<<<"), treeMaker.Literal(tag), treeMaker.Literal(methodName), timeoutStatement.getExpression())
                )
        );

//        catch中的代碼塊
        JCTree.JCBlock catchBlock = treeMaker.Block(0, List.of(
                treeMaker.Throw(
//                        e 這個字符是catch塊中定義的變量
                        treeMaker.Ident(getNameFromString("e"))
                )
        ));
//        finally代碼塊中的代碼
        JCTree.JCBlock finallyBlock = treeMaker.Block(0, List.of(TakeTime));


        List<JCTree.JCStatement> statements = jcMethodDecl.body.getStatements();
//        遍歷方法體中每一行(斷句符【分號/大括號】)代碼
        for (JCTree.JCStatement statement : statements) {
            messager.printMessage(Diagnostic.Kind.NOTE, "遍歷方法體中的statement:" + statement);
            messager.printMessage(Diagnostic.Kind.NOTE, "該statement的類型:" + statement.getKind());
            if (statement.getKind() == Tree.Kind.RETURN) {
                messager.printMessage(Diagnostic.Kind.NOTE, "該statement是Return語句");
                break;
            }

        }

//        jcMethodDecl.body即為方法體,利用treeMaker的Block方法獲取到一個新方法體,將原來的替換掉
        jcMethodDecl.body = treeMaker.Block(0, List.of(
//                定義開始時間,並附上初始值 ,初始值為編譯時的時間
                startTime,
                treeMaker.Exec(
//                        這一步 將startTime變量進行賦值 其值 為(表達式也即運行時時間) startTime = System.currentTimeMillis()
                        treeMaker.Assign(
                                treeMaker.Ident(getNameFromString("startTime")),
                                time.getExpression()
                        )
                ),
//                添加TryCatch
                treeMaker.Try(jcMethodDecl.body,
                        List.of(treeMaker.Catch(createVarDef(treeMaker.Modifiers(0), "e", memberAccess("java.lang.Exception"),
                                null), catchBlock)), finallyBlock)

//                下面這段是IF代碼,是我想在try catch finally后添加return代碼(如果有需要的話),結果發現 如果不寫下面的代碼的話
//                Javac會進行判斷,如果這個方法有返回值的話,那么Javac會自動在try塊外定義一個變量,同時找到要上一個return的變量並賦值
//                然后返回,具體可以查看編譯后的字節碼的反編譯文件,如果該方法沒有返回值,那么什么也不做

//                根據返回值類型,判斷是否在方法末尾添加 return  語句  判斷返回類型的Kind是否等於TypeKind.VOID
//                treeMaker.If(treeMaker.Parens(
//                        treeMaker.Binary(
//                                JCTree.Tag.EQ,
//                                treeMaker.Literal(returnType.getKind().toString()),
//                                treeMaker.Literal(TypeKind.VOID.toString()))
//                        ),
//
//                        //符合IF判斷的Statement
//                        treeMaker.Exec(treeMaker.Literal("返回類型是Void,不需要return")),
////                        不符合IF判斷的Statement
//                        null
//                )
                )


        );


        return jcMethodDecl;
    }


    /**
     * 創建變量語句
     *
     * @param modifiers
     * @param name      變量名
     * @param varType   變量類型
     * @param init      變量初始化語句
     * @return
     */
    private JCTree.JCVariableDecl createVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression varType, JCTree.JCExpression init) {
        return treeMaker.VarDef(
                modifiers,
                //名字
                getNameFromString(name),
                //類型
                varType,
                //初始化語句
                init
        );
    }


    /**
     * 根據字符串獲取Name,(利用Names的fromString靜態方法)
     *
     * @param s
     * @return
     */
    private com.sun.tools.javac.util.Name getNameFromString(String s) {
        return names.fromString(s);
    }


    /**
     * 創建 域/方法 的多級訪問, 方法的標識只能是最后一個
     *
     * @param components
     * @return
     */
    private JCTree.JCExpression memberAccess(String components) {
        String[] componentArray = components.split("\\.");
        JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
        for (int i = 1; i < componentArray.length; i++) {
            expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
        }
        return expr;
    }


}

 

這里我自定義的注解處理器,主要操作Javac在編譯被注解標注的方法時,在生成字節碼時添加自己的邏輯

首先在 方法體的開頭 插入一條當前時間的變量 ,並賦值為 System.currentTimeMillis()

然后將整個方法體包括在try塊中,添加catch 即finally 

catch塊中直接定義異常並跑出,finally塊中打印用時語句!

之所以添加try catch finally語句,主要是考慮有的方法是在if判斷中返回,所以如果在每個return前插入打印用時代碼,就十分麻煩

所以直接使用try塊包裹方法體來實現!

另外為什么catch塊中直接要出異常?因為如果原來的方法體捕獲了異常,那么自然不會走自己創建的catch塊,如果沒有捕獲,那么自定義的catch塊

會把這個異常原封不動的拋出去,這樣並不影響原來的業務了!

直接說並不直觀,放兩張圖片說明問題

這張圖是源碼

 

 這是編譯后的源碼,編譯后的源碼是.class文件,我用IDEA直接打開class文件 就是反編譯后的文件

 

 

 可以看到反編譯后的代碼,添加了時間變量,和try塊代碼,需要注意的是,startTime被賦了初始值,這個值其實是這個class被編譯的時間,這個初始值也可以設置為其他值,當然必須是Long類型的,或者設置為null,我這里設置這個時間主要是用來測試!建議設置為null

最后看一下 原方法的打印結果:

 

因為源碼中 sleep了3秒中,所以最后直接時間是3001毫秒

同時 mmm 是 注解中的自定義的tag的值,之所以設置這個,是因為如果方法較多時,方便進行查找等!(其實是參照的Android的Logger)

 

 

關於注解處理器的文章有很多,我這里只寫一些自己認為比較重要的!

1.關於@AutoServier注解 :如果使用的是maven話,那么直接引入google的 auto-service依賴即可,如果使用的是gradle的話且版本在5(包含)之后

需要添加兩條依賴:

//    聲明注解處理器的注解,用於代替手動編輯resources/META-INF/services的文件
    compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
//    這行配置也需要添加,gradle升級到5之后,不加此配置,不會生成META-INF/services/javax.annotation.processing.Processor文件
    annotationProcessor group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'

如果當注解處理器打完包后,被其他項目(gradle)引用,也要 使用 compile / annotationProcessor 來引入兩次 

 

2.理解幾個常見的類 :

 聲明變量 
 
JCTree.JCVariableDecl
定義變量 long a = 1
JCTree.JCExpressionStatement
生成表達式
a = System.currentTimeMillis()
JCTree.JCBlock
代碼塊(主要是用來放其他代碼塊,或者JCExpressionStatement的)  

 

 

 

 

關於 更多API參照:https://blog.csdn.net/a_zhenzhen/article/details/86065063

簡單用法參照:https://blog.csdn.net/dap769815768/article/details/90448451

 源碼:https://github.com/chenwuwen/annotation_processor


免責聲明!

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



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