1. 程式人生 > >Java中實現自定義的註解處理器(Annotation Processor)

Java中實現自定義的註解處理器(Annotation Processor)

在之前的《簡單實現ButterKnife的註解功能》中,使用了執行時的註解實現了通過編寫註解繫結View與xml。由於執行時註解需要在Activity初始化中進行繫結操作,呼叫了大量反射相關程式碼,在介面複雜的情況下,使用這種方法就會嚴重影響Activity初始化效率。而ButterKnife使用了更高效的方式——Annotation Processor來完成這一工作。

Annotation Processor即為註解的處理器。與執行時註解RetentionPolicy.RUNTIME不同,Annotation Processor處理RetentionPolicy.SOURCE型別的註解。在java程式碼編譯階段對標註RetentionPolicy.SOURCE

型別的註解進行處理。這樣在編譯過程中新增程式碼,效率就非常高了。同樣,Annotation Processor也可以實現IDE編寫程式碼時的各種程式碼檢驗,例如當你在一個並未覆寫任何父類方法的函式上添加了@Override註解,IDE會紅線標識出你的函式提示錯誤。

實現步驟

使用Annotation Processor需要實現AbstraceProcessor這個抽象類,並配置工程引用這個Processor。
以下從Gradle編譯工程及Eclipse中配置兩方面介紹如何自定義並使用Annotation Processor。

Gradle編譯環境:
1.實現Annotation Processor
2.配置Processor工程的META_INF檔案
3.在開發的程式碼中使用自定義註解
4.配置gradle編譯指令碼,引入processor工程
5.進行專案構建,檢視processor輸出

Eclipse環境:
1.將Gradle環境編譯出的processor.jar作為庫引入到工程中
2.配置當前工程支援Annotation Processor,並使用自定義的processor.jar檔案
3.開發程式碼使用自定義註解,檢視IDE上提示資訊

*IDEA環境的配置與Eclipse類似,官網上已經有比較詳細的描述了,可以查閱Jetbrain的官方文件。

Gradle環境

構建工程目錄

先來看一下processor工程的構建。
假設在HelloWorld工程中使用自定義的processor;獨立於HelloWorld工程,我們獨立開發了自定義的processor工程。專案結構如下:

MyProcessorTest
│          
├─MyProcessor
│  │  
│  └─src
│      └─main
│          └─java
│             └─com
│                 └─processor
│                         MyProcessor.java
│                         TestAnnotation.java
│                          
└─src
    └─main
        └─java
            └─com
                └─hello
                        HelloWorld.java

主工程名為MyProcessorTest,在其中包含了processor工程MyProcessor

實現自定義註解

接下來實現一個自定義註解TestAnnotation

package com.processor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface TestAnnotation {
    int value();
    String what();
}

注意註解的Retention是RetentionPolicy.SOURCE型別。

建立自定義Annotation Processor

然後來實現自定義的Annotation Processor——MyProcessor

package com.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes({"com.processor.TestAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Test log in MyProcessor.process");
        return false;
    }

}

其中幾個要點:
1.自定義Annotation Processor需要繼承AbstractProcessor,並覆寫process方法。
2.需要宣告此Processor所支援處理的註解類
@SupportedAnnotationTypes({"com.processor.TestAnnotation"})
(類名需要已字串形式填入包名+類名,否則不起作用)
3.需要宣告註解作用的java版本,由於我的工程預設使用了JDK 7進行編譯,因此需要填寫
@SupportedSourceVersion(SourceVersion.RELEASE_7)

目前自定義的Processor僅列印了一條log,如果成功的話,會在命令列編譯時看到這條列印。
由於自定義Processor類最終是通過打包成jar,在編譯過程中呼叫的。為了讓java編譯器識別出這個自定義的Processor,需要配置META-INF中的檔案,將這個自定義的類名註冊進去。
javax.annotation.processing.Processor檔案:

com.processor.MyProcessor

新增完META-INF後的MyProcessor工程結構如下:

├─MyProcessor
│  │  
│  └─src
│      └─main
│          ├─java
│          │  └─com
│          │      └─processor
│          │              MyProcessor.java
│          │              TestAnnotation.java
│          │              
│          └─resources
│              └─META-INF
│                  └─services
│                          javax.annotation.processing.Processor

這樣自定義Processor的基本雛形就完成了。

引用自定義註解

接下來編寫HelloWorld類,引入自定義註解:

package com.hello;

import com.processor.TestAnnotation;

public class HelloWorld {

    @TestAnnotation(value = 5, what = "This is a test")
    public static String msg = "Hello world!";

    public static void main(String[] args) {
        System.out.println(msg);
    }

}

在變數msg上使用了註解。

配置Gradle編譯環境

下面來配置Gradle編譯環境。
首先可以從一個Android工程裡拷貝一份Gradle Wrapper到工程目錄下(gradlew, gradlew.bat, 以及gradle目錄),這時的工程結構(僅根目錄下及gradle有關子目錄):

│  gradlew
│  gradlew.bat
│              
├─gradle
│  └─wrapper
│          gradle-wrapper.jar
│          gradle-wrapper.properties
│          
├─MyProcessor
│  └─src
│                      
└─src
    └─main

此時還沒有build.gradlesettings.gradle檔案,因為我們有了gradle wrapper,因此可以通過它來自動生成這兩個檔案。執行命令

gradlew.bat init

這樣就生成預設的gradle配置檔案了。接下來修改兩個配置檔案。
首先在settings.gradle中新增processor工程,以便在根目錄下直接編譯兩個工程,以及後續的依賴配置。
settings.gradle

rootProject.name = 'MyProcessorTest'
include 'MyProcessor'

然後在build.gradle中宣告依賴,以便HelloWorld中自定義註解的處理以及processor的引用
build.gradle:

apply plugin: 'java'

dependencies {
    compile project('MyProcessor')
}

上面的操作僅配置了根目錄下的gradle配置,但MyProcessor中還沒有配置。在MyProcessor的根目錄下新建一個build.gradle即可:
build.gradle:

apply plugin: 'java'

執行自定義Processor

接下來就可以編譯專案了,在根目錄下執行

gradlew.bat assemble

輸出log:

Executing command: ":assemble"
:MyProcessor:compileJava UP-TO-DATE
:MyProcessor:processResources UP-TO-DATE
:MyProcessor:classes UP-TO-DATE
:MyProcessor:jar UP-TO-DATE
:compileJava
Test log in MyProcessor.process
Test log in MyProcessor.process
:processResources UP-TO-DATE
:classes
:jar
:assemble

BUILD SUCCESSFUL

Total time: 7.353 secs

Completed Successfully

可以看到已經輸出了Test log in MyProcessor.process,證明自定義的processor已經起作用了!
但是這裡為何輸出兩次log?
在程式碼中新增幾行呼叫:

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Test log in MyProcessor.process");
        System.out.println(roundEnv.toString());    //列印傳入的roundEvn物件資訊
        for (TypeElement element : annotations) {
            System.out.println(element.getSimpleName());    //遍歷annotation,打印出註解型別
        }
        return false;
    }

再次執行log輸出:

Compiling with JDK Java compiler API.
Test log in MyProcessor.process
[errorRaised=false, rootElements=[com.hello.HelloWorld], processingOver=false]
TestAnnotation
Test log in MyProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]
:compileJava (Thread[main,5,main]) completed. Took 0.249 secs.

可以看出僅在第一次process處理了TestAnnotation註解,第二次並沒有遍歷到;並且processingOver狀態不同。這裡的具體流程還沒搞懂,先略過……

添加註解處理及資訊提示

最後一步,就要為註解程式新增真正的處理功能了。直接放程式碼:

@SupportedAnnotationTypes({"com.processor.TestAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Test log in MyProcessor.process");
        System.out.println(roundEnv.toString());

        for (TypeElement typeElement : annotations) {    // 遍歷annotations獲取annotation型別
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {    // 使用roundEnv.getElementsAnnotatedWith獲取所有被某一型別註解標註的元素,依次遍歷
                // 在元素上呼叫介面獲取註解值
                int annoValue = element.getAnnotation(TestAnnotation.class).value();
                String annoWhat = element.getAnnotation(TestAnnotation.class).what();

                System.out.println("value = " + annoValue);
                System.out.println("what = " + annoWhat);

                // 向當前環境輸出warning資訊
                processingEnv.getMessager().printMessage(Kind.WARNING, "value = " + annoValue + ", what = " + annoWhat, element);
            }
        }
        return false;
    }

}

這次僅執行gradlew.bat compileJava

Executing command: ":compileJava"
:MyProcessor:compileJava
:MyProcessor:processResources UP-TO-DATE
:MyProcessor:classes
:MyProcessor:jar
:compileJava
Test log in MyProcessor.process
[errorRaised=false, rootElements=[com.hello.HelloWorld], processingOver=false]
D:\test\MyProcessorTest\src\main\java\com\hello\HelloWorld.java:8: 警告: value = 5, what = This is a test
    public static String msg = "Hello world!";
                         ^
1 個警告
value = 5
what = This is a test
Test log in MyProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]

BUILD SUCCESSFUL

Total time: 9.048 secs

Completed Successfully

自定義processor已經起作用了。最後的

processingEnv.getMessager().printMessage(Kind.WARNING, "value = " + annoValue + ", what = " + annoWhat, element);

在命令列環境中丟擲了warning。
實際processingEnv不僅可以作用於命令列,對IDE同一樣生效。

Eclipse環境配置

下面再看看目前編譯好的Processor如何在Eclipse環境中生效。

步驟如下:
1.在當前工程中開啟Annotation Processing。
通過工程屬性,開啟Java Compiler->Annotation Processing中的選項;
開啟Annotation
開啟Annotation

繼續設定Java Compiler->Annotation Processing -> Factory Path,匯入自定義Processer的jar檔案(匯入剛剛編譯出的MyProcessor.jar)。
使用自定義的Processor
使用自定義的Processor

2.在程式碼中引用註解,顯示自定義Processor中的提示資訊:
顯示Processor中的警告
顯示Processor中的警告

這時Eclipse中的工程結構:
工程結構

Eclipse的官方文件中給了一個驗證用的APTDemo.jar。實際反編譯後可以看到jar中使用了Java 5的註解處理器API實現的功能(包名還是sun的)。
而在上面的樣例程式碼中,使用的是Java 6中的API實現的。

另外在Eclipse的官方文件:
Eclipse官方文件
JDT Plug-in Developer Guide > Programmer’s Guide > JDT Annotation Processing
一節中,說道:

Eclipse 3.2 provided support for annotation processors using the Java 5 Mirror APIs, and Eclipse 3.3 added support for processors using the Java 6 annotation processing APIs.

也就是以上的方法對Eclipse 3.3以上版本有效。
同時還有:

Eclipse does not support executing Java 6 processors while typing in the editor; you must save and build in order for Java 6 processors to report errors or generate files.

然而至少通過Mars版本(4.5.2)的Eclipse,是可以在編輯器中看到warning資訊的,有可能是文件這裡仍沒有更新……