Java註解解析-搭建自己的註解處理器(CLASS註解使用篇)

那個人發表於2019-03-02

寫在前面

該文章是繼Java註解解析-基礎+執行時註解(RUNTIME)之後,使用註解處理器處理CLASS註解的文章。通過完整的Demo例子介紹整個註解處理器的搭建流程以及注意事項,你將知道如何去搭建自己的註解處理器。前提是你知道如何去寫自定義註解,不清楚的可以點選我上面的連結哦~

介紹

顧名思義註解處理器就是javac包中專門用來處理註解的工具。所有的註解處理器都必須繼承抽象類AbstractProcessor然後重寫它的幾個方法。註解處理器是執行在它自己的JVM中。javac啟動一個完整Java虛擬機器來執行註解處理器,這意味著你可以使用任何你在其他java應用中使用的的東西。其中抽象方法process是必須要重寫的,再該方法中註解處理器可以遍歷所有的原始檔,然後通過RoundEnvironment類獲取我們需要處理的註解所標註的所有的元素,這裡的元素可以代表包,類,介面,方法,屬性等,具體的解釋放在下一章節,因為這裡面牽扯到的東西實在是太多了。再處理的過程中可以利用特定的工具類自動生成特定的.java檔案或者.class檔案,來幫助我們處理自定義註解。

下面開始搭建

1.建立

首先註解處理器需要javax包的支援,我們的Android環境下是訪問不到javax包的,除非我們自己配置。

//app:build.gradle中加入依賴,一定要使用provided files來引入.
provided files('/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/rt.jar')
複製程式碼

所以我們需要建立Java Library包來提供javax環境,另外註解處理器要被打包進jar包裡面才能被系統識別,這就是選用ava Library的原因,目前註解註解框架均是如此。

image

建立好Module之後就可以寫我們的自定義的註解處理器了。首先需要繼承抽象類AbstractProcessor,然後重寫process()方法。該方法是核心方法,該方法將遍歷原始碼,找出我們想要註解標註的元素。單單重寫這一個方法是不夠的, 在我們的開發中往往需要重寫init()getSupportedSourceVersion()getSupportedAnnotationTypes()這幾個方法就可以了。另外再Java7及其以後我們還可以使用註解@SupportedSourceVersion()@SupportedAnnotationTypes() 去替代上面的方法,見於該註解有Java版本的限制,所以還是建議直接重寫方法為好。

public class AnnotationTwoProcessor extends AbstractProcessor {

    private Messager messager; //用於列印日誌
    private Elements elementUtils; //用於處理元素
    private Filer filer;  //用來建立java檔案或者class檔案

    //該方法再編譯期間會被注入一個ProcessingEnvironment物件,該物件包含了很多有用的工具類。
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
    }

    /**
     * 該方法將一輪一輪的遍歷原始碼
     * @param set  該方法需要處理的註解型別
     * @param roundEnvironment 關於一輪遍歷中提供給我們呼叫的資訊.
     * @return  改輪註解是否處理完成 true 下輪或者其他的註解處理器將不會接收到次型別的註解.用處不大.
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        return false;
    }

    /**
     * 返回我們Java的版本.
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

    /**
     * 返回我們將要處理的註解
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<>();
        annotataions.add(MyAnnotion.class.getCanonicalName());
        return annotataions;
    }
}
複製程式碼

2.註冊

註冊註解處理器的方法有兩種:

第一種: 處理器必須被打包進一個jar包裡面,這也是為什麼要建立一個Java Module。該jar包需要有一個特定路徑resources/META-INF/services的檔案javax.annotation.processing.Processor,該檔案的路徑和名稱都是特定的,然後將我們宣告註解處理器的全路徑寫到該檔案中,這樣Java虛擬機器會自動找該路徑下中我們宣告的處理器。

image

我們再建立資料夾的時候一定要確定其命名的準確性。建立檔案的時候直接右鍵->new file,保證我們的檔案的名字為javax.annotation.processing.Processor

image
建立成功之後我們將自定義的註解處理器的全路徑寫到該檔案裡面。

image

這樣問題來了,如我我們寫了多個註解處理器該怎麼宣告呢?接著看。如果我們一個Module裡面宣告瞭多個註解處理器,再該檔案中宣告的時候每個註解處理器要換行宣告,執行的順序就按宣告的順序去執行。這裡需要對註解處理器的執行順序解釋一下,再編譯期間並不是一個註解處理器完全的處理結束再開始下一個的,而是在掃描一輪原始碼 的時候註冊的第一個處理器先執行一輪,然後註冊的第二個處理器開始執行然後。。。三個。。四個。第二輪的時候還是註冊的第一個處理器先執行,然後第二個。。。三個。。。這裡面的深刻解釋會在下一篇講解,這篇只是使用。

第二種: 當覺得第一種方法配置繁瑣的時候就會有新的更簡潔的方式出現。谷歌公司推出的使用註解去註冊註解處理器。

  1. 新增依賴,可以去GitHub上面查詢最新版本。

    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    複製程式碼
  2. 我們就可以使用了,它的作用和我們手寫的作用是一樣的。不過註釋的處理器的處理順序跟你類建立的順序是一致的,跟方法一中檔案宣告的順序不一樣。

    @AutoService(Processor.class)
    public class AnnotationTwoProcessor extends AbstractProcessor {
    }
    複製程式碼

總的來說方式1的註冊方式目前僅在EventBus裡面有用到,方式2還是推薦使用的,比如:Arouter,BufferKnife等流行框架都是採用的這種方式註冊的,方便快捷。

3.APP引用註解處理器

再引用註解處理器的Module的時候坑其實挺多的。首先我們得確保處理器所在的jar包會新增到我們執行的專案中,註解處理器才會被得到執行,這裡呢因為原始碼不清楚,所以只好自己去試了。Application引入註解處理器包的時候並不像我們引入其它的Android Module一樣,這裡列舉三種種引入方法。

  • plugin: 'com.android.application'

    我們可以使用implementation,compile,api直接引用註解處理器的module,但是會有一個編譯錯誤:

    Error:Execution failed for task ':app:javaPreCompileDebug'.
    > Annotation processors must be explicitly declared now.  The following dependencies on the compile classpath are found to contain annotation processor.  Please add them to the annotationProcessor configuration.
        - annotation_processor.jar (project :annotation_processor)
      Alternatively, set android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true to continue with previous behavior.  Note that this option is deprecated and will be removed in the future.
      See https://developer.android.com/r/tools/annotation-processor-error-message.html for more details.
    複製程式碼

    我們需要加一段程式碼,程式碼位置如下所示,這樣就可以愉快的引入註解處理器了:

    android {
        defaultConfig {
            javaCompileOptions {
                annotationProcessorOptions {
                    includeCompileClasspath = true
                }
            }
        }
    }
    複製程式碼

    另外我們可以用annotationProcessor引入註解處理器,這個引入方式是為了替換Gradle版本2.2之前的apt引入方法。該引入方式專門用來處理註解處理器。使用該引入方式的時候不會出現錯誤提示,也是Android中推薦使用的引入方法,該方式也是主流方式。

    annotationProcessor project(':annotation_processor')
    複製程式碼
  • apply plugin: 'com.android.library'

    前提module一定要被android application引入。module裡面引入註解處理器的module的話,基本跟android application中一致,這裡說一下兩個不同點annotationProcessor和implementation方式的引入都不會執行註解處理器,真實的原理並不清楚,只能猜測是application才能正真的處理註解,所以得把依賴暴露出去。這點再android library的module中一定得注意。不太建議該方式引入。

  • apply plugin: 'java-library'

    前提java library一定要被android application引入。宣告這樣的module 的話我們就可以直接引入註解處理器了,也不用擔心用什麼方式引入。不過這種場景不太多。

如何確保你的註解處理器已經註冊成功了。首先你已經自定義好了一個註解,並且該註解已經用到了你的程式碼裡面。如下程式碼所示,你已經設定了我們要處理的是那種型別的註解(程式碼1所示),然後再我們的process方法裡面打上日誌(程式碼2所示),然後點選Android StudioMake Project按鈕,之後開啟Gradle Console視窗看build資訊,當你發現資訊中列印了程式碼2所示的資訊之後就表明你的註解處理器已經執行起來了。如果沒有列印資訊的話嘗試 clean一下專案然後重新Make Project。如果發現沒有列印日誌的話,嘗試檢視註解處理器是否已經註冊和註解處理器所在的module是否被android application成功引入。

@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {

    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //程式碼2
        messager.printMessage(Diagnostic.Kind.NOTE,"日誌開始---------------");
        return false;
    }


    //程式碼1
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedOptions = new HashSet<>();
        supportedOptions.add(MyAnnotion.class.getCanonicalName());
        return supportedOptions;
    }
    
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }
}
複製程式碼

4.自動生成Java檔案

走到該步驟的時候,你要確保你的註解處理器已經正常的執行。我們使用註解處理器處理原始碼註解的目的,就是再編譯期間完成我們對某個註解的處理工作。比如:BufferKnife再編譯期間會在使用特定註解的檔案路徑生成***—ViewBinding的原始檔去處理特定註解。這裡用一個Demo去演示如何生成程式碼:

先看我的註解:

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.LOCAL_VARIABLE})
public @interface MyAnnotion {
    String value() default "ssssss";

}
複製程式碼

在我的MainActivity上面使用註解:

@MyAnnotion()
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
複製程式碼

接著使用註解處理器去處理註解,生成對應的MainActivity_ViewBinding原始檔。

public class AnnotationProcessor extends AbstractProcessor {

    private Messager messager;
    private Elements elementUtils;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();

    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        messager.printMessage(Diagnostic.Kind.NOTE,"日誌開始---------------");

        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MyAnnotion.class);
        for (Element element:elementsAnnotatedWith) {
            if(element.getKind() == ElementKind.CLASS){
                TypeElement typeElement = (TypeElement) element;
                PackageElement packageElement = elementUtils.getPackageOf(element);
                String packagePath = packageElement.getQualifiedName().toString();
                String className = typeElement.getSimpleName().toString();
                try {
                    JavaFileObject sourceFile = filer.createSourceFile(packagePath + "." + className + "_ViewBinding", typeElement);
                    Writer writer = sourceFile.openWriter();
                    writer.write("package  "+packagePath +";\n");
                    writer.write("import  "+packagePath+"."+className+";\n");
                    writer.write("public class "+className+"_ViewBinding"+"  { \n");
                    writer.write("\n");
                    writer.append("       public "+className +"  targe;\n");
                    writer.write("\n");
                    writer.append("}");
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }


        messager.printMessage(Diagnostic.Kind.NOTE,"日誌結束---------------");
        return false;
    }


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedOptions = new HashSet<>();
        supportedOptions.add(MyAnnotion.class.getCanonicalName());
        return supportedOptions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }
}
複製程式碼

結果展示:

Java註解解析-搭建自己的註解處理器(CLASS註解使用篇)

注意一下生成的位置!我們可以直接再我們正常的程式碼中應用到該檔案,因為該檔案是會生成class檔案的。

5.總結

該文章只是介紹瞭如何搭建起一個Java註解處理器,沒有更深入的去講解AbstractProcessor類以及我們再處理註解的過程中用到的各種類的API。當然接下來的文章就會詳細的介紹註解處理器所使用到的類,方法,屬性等的用法和意義,這一定是史上最全的註解處理器API。之後你會更加隨心所欲的去構建自己的註解框架。

下章節 史上最全的註解處理器API指導!

歡迎大家留言提出文章中出現的錯誤以及不理解的地方,會在第一時間進行更正和解答~

我的掘金
我的簡書
我的CSDN
我的Github
Demo地址,歡迎star

相關文章