Java中實現自定義的註解處理器(Annotation Processor)
在之前的《簡單實現ButterKnife的註解功能》中,使用了執行時的註解實現了通過編寫註解繫結View與xml。由於執行時註解需要在Activity初始化中進行繫結操作,呼叫了大量反射相關程式碼,在介面複雜的情況下,使用這種方法就會嚴重影響Activity初始化效率。而ButterKnife使用了更高效的方式——Annotation Processor來完成這一工作。
Annotation Processor即為註解的處理器。與執行時註解RetentionPolicy.RUNTIME
不同,Annotation Processor處理RetentionPolicy.SOURCE
型別的註解。在java程式碼編譯階段對標註RetentionPolicy.SOURCE
@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.gradle
和settings.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
繼續設定Java Compiler->Annotation Processing -> Factory Path,匯入自定義Processer的jar檔案(匯入剛剛編譯出的MyProcessor.jar)。
使用自定義的Processor
2.在程式碼中引用註解,顯示自定義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資訊的,有可能是文件這裡仍沒有更新……