在 Java 5 導入標註時,其實就提供了個標註處理工具(Annotation Processing Tool, APT),可以使用非標準的 com.sun.mirror
等 API 來撰寫註解處理器,再透過 apt
工具程式,於靜態時期處理標註。
接著在 Java 6 中納入了 JSR269:Pluggable Annotation Processing API,標準化了標註處理器的 API(套件為 javax.annotation.processing
、 javax.lang.model
等),而 Java 7 將 apt
工具與原先的非標準 API 標為廢棄(deprecated),後續在 Java 8 正式移除 apt
與 com.sun.mirror
等相關 API。
具體來說,現在開發者可以繼承 AbstractProcessor
(實作了 javax.annotation.processing.Processor
),使用 @SupportedSourceVersion
指定 Java 版本,@SupportedAnnotationTypes
指定要處理的標註類型名稱,並定義 process
方法來處理標註,在 javac
編譯時若使用 -processor
或 --processor-path
指定標註處理器來源,或者類別路徑包含的 JAR 中,META-INF 中有 javax.annotation.processing.Processor 檔案,其中撰寫了處理器類別全名,在編譯器剖析、生成語法樹之後,若原始碼中有指定要處理的標註,就會載入標註處理器並執行 process
方法。
例如,若你撰寫了 Debug
標註:
package cc.openhome;
public @interface Debug {
}
在 Some
上做了標註:
package cc.openhome;
@Debug
public class Some {
@Debug
public void test() {
}
public void test2() {
}
}
若要在編譯時期處理 @Debug
資訊,可以寫個處理器:
package cc.openhome;
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;
@SupportedSourceVersion(SourceVersion.RELEASE_10)
@SupportedAnnotationTypes({ "cc.openhome.Debug" })
public class DebugProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println(roundEnv.processingOver());
System.out.println(roundEnv.getRootElements());
System.out.println(annotations);
return false;
}
}
若使用 javac
編譯 Some
時,類別路徑中可以找到這個處理器,而且使用 -processor
或 JAR 中包含了這個處理器的資訊,就會看到底下訊息,例如:
> javac --release 10 -cp bin -processor cc.openhome.DebugProcessor -d bin src/cc/openhome/Some.java
false
[cc.openhome.Some]
[cc.openhome.Debug]
true
[]
[]
如果在 process
中修改了語法樹,編譯器就會回到上一步重新處理,然後再次執行 process
,每次執行 process
稱為一個 round,可以透過 RoundEnvironment
來取得語法樹元素資訊,例如 getRootElements
取得了前一個 round 時處理的根元素(在這邊就是類別),這個循環會進行到語法樹沒有任何修改,然後進入 last round,可以透過 processingOver
來得知是否 last round。
process
最後傳回的 boolean
值表示,該處理器是否已經完成了支持的標註處理,其他處理器可以不用進一步處理了。
當然,修改語法樹基本上是不建議的,不過 Java 的生態圈裏,有個 Lombok 就是這麼做的,它運用了非標準的Java Compiler Tree API(com.sun.tools.javac
等 API)修改語法樹,之後將修改過的結果交由編譯器分析、產生位元組碼並儲存為 .class。
通常建議將標註處理器用於原始碼檢查、程式碼產生器,這就需要知道原始碼的資訊,在編譯時期,javac
本身會運行在一個 JVM,這也是為何你可以如上使用 Java 程式碼來運用 Processor
的原因,而原始碼資訊,主要會運用 javax.lang.model.type
、javax.lang.model.element
中的相關 API 來封裝(而不是透過 Reflection API,因為 Reflection API 只能用在執行時期)。
例如,若想在編譯時期,找出 Some
上被 @Debug
標註的元素:
package cc.openhome;
import java.util.List;
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.Element;
import javax.lang.model.element.TypeElement;
@SupportedSourceVersion(SourceVersion.RELEASE_10)
@SupportedAnnotationTypes({"cc.openhome.Debug"})
public class DemoProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getRootElements();
for(Element root: elements) {
if(root.getAnnotation(Debug.class) != null) {
System.out.println(root);
}
List<? extends Element> enclosedElems = root.getEnclosedElements();
for(Element elem : enclosedElems) {
if(elem.getAnnotation(Debug.class) != null) {
System.out.println(elem);
}
}
}
return false;
}
}
在編譯時指定此標註處理器,就會看到底下資訊:
cc.openhome.Some
test()
Google 的 AutoValue 就是基於 JSR269,可自動為被標註類別產生具有 Getter、Setter 等方法的原始碼,有興趣的話,可以看看它的原始碼是怎麼實作的,從中認識更多編譯時期處理標註的方式。