Java語言使用註解處理器生成代碼——第二部分:註解處理器

原文作者:deors
原文地址:https://deors.wordpress.com/2011/10/08/annotation-processors/
譯文作者:Jianan - [email protected]
版本信息:本文基於2015-10-16版本進行翻譯
版權聲明:本文經原文作者許可進行翻譯,保留所有權利,未經允許不得複製和轉載


本文是我的“關於Java語言使用註解處理器生成代碼”系列第二部分。在第一部分中(請閱讀這裏),我們介紹了什麼是Java語言的註解,以及它們的幾種常用方式。

現在,在這第二部分中,我們將介紹註解處理器。包括如何創建註解處理器,以及如何運行它們。



毫無疑問,註解是非常優秀的。你可以通過註解來設置任何類型的元數據和配置,並且註解有非常容易定義的語法,以及多種不同的類型可以使用。

直到目前爲止,我們所看到的是註解相比於Javadoc有很多優點,但是還不足夠證明它們集成到Java語言的程度。因此,是否有可能與註解進行交互並從註解中獲得更多的功能?答案是肯定的:

  • 在運行過程中,使用RUNTIME生命週期類型的註解能夠通過反射來訪問。通過Class的getAnnotation()和getAnnotations()方法就能完成這件奇妙的事情[1]。

  • 在編譯過程中,註解處理器,一種特殊的類,能夠處理從編譯的代碼中找到的每一個註解。


註解處理器的API


在Java 5首次引入註解的時候,註解處理器的API還沒有成熟,也沒有標準化。處理註解需要一個名爲apt(也就是Annotation Processor Tool,註解處理器工具)的獨立的工具,以及包含在com.sum.mirror包中的Mirror API。apt需要使用Mirror API來自定義處理器。

從Java 6開始,註解處理器通過JSR 269[2]已經標準化並被納入到標準庫中,apt工具也被無縫集成到Java編譯工具javac裏面。

儘管我們只討論Java 6新註解處理器API的細節,但是你同樣可以在這裏或者這裏找到Java 5文檔中關於apt以及Mirror API的相關信息,並在這篇文章中找到適當的例子。

註解處理器不是簡單地實現javax.annotaion.processing.Processor接口並完成其中定義的方法。因此,爲方便我們實現自定義處理器,Java提供了一個已經實現通用功能的虛擬類javax.annotation.processing.AbstractProcessor。

自定義的註解處理器可能會用到下面三個註解來配置自己:

  • javax.annotation.processing.SupportedAnnotationTypes:用於給註解處理器註冊能夠處理的註解類型。可用值爲支持的註解類型完整名稱——使用通配符也是允許的。

  • javax.annotation.processing.SupportedSourceVersion:用來聲明註解處理器所支持的源代碼版本。

  • javax.annotation.processing.SupportOption:用來註冊可能從命令行傳遞過來的自定義選項(參數)。

最後,我們要實現自己的process()方法。


編寫我們的第一個註解處理器


接下來讓我們開始編寫我們的第一個註解處理器。遵循上面章節描述的常規步驟,我們將創建一個註解處理器類,它能夠處理第一部分章節中示例代碼的Complexity註解:

package sdc.assets.annotations.processors;

import@SupportedAnnotationTypes("sdc.assets.annotations.Complexity")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ComplexityProcessor extends AbstractProcessor {

    public ComplexityProcessor() {
        super();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        return true;
    }
}

上面這個註解處理器類註冊了它所支持的註解類型爲sdc.assets.annotations.Complexity,雖然這個註解處理器被調用的時候什麼事都沒幹。這樣,每次Java編譯器(編譯)生成class時,如果有發現使用了Complexity註解就會執行這個註解處理器,這個註解處理器必須能夠在classpath中找到(稍後我們會了解這方面的更多細節)。

爲了能夠與被註解的類進行交互,process()方法提供兩個參數:

  • 一個java.lang.model.TypeElement對象的Set集合:處理註解的過程要經過一個或者多個回合才能完成。每個回合中註解處理器都會被調用,並且接收到一個以在當前回合中已經處理過的註解類型的Type爲元素的Set集合。

  • 一個javax.annotation.processing.RoundEnvironment對象:通過這個對象可以訪問到當前或者之前的回合中處理的Element元素(譯註:可以被註解類型註解的元素,如類、方法、參數等等),只有被註解處理器註冊的註解類型註解過的元素纔會被處理。

除了上面兩個參數之外,還有一個ProcessingEnvironment類型的實例對象processingEvn可以使用。通過這個對象可以訪問日誌或者其它一些工具;其中一些會在稍後討論。

通過RoundEnvironment對象以及被註解元素的反射方法,我們就可以寫一個簡單的註解處理器實現類。這個註解處理器的處理方法只是打印出所有能找得到的,被Complexity註解的元素:

for (Element elem : roundEnv.getElementsAnnotatedWith(Complexity.class)) {
    Complexity complexity = elem.getAnnotation(Complexity.class);
    String message = "annotation found in " + elem.getSimpleName()
                   + " with complexity " + complexity.value();
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
}
return true; // no further processing of this annotation type


打包並註冊註解處理器


完成我們的註解處理器的最後一個步驟是打包並註冊。這樣Java編譯器或者其它工具才能夠找到這個註解處理器。

註冊處理器最簡單的方法就是利用標準的Java服務機制:
* 將你的註解處理器打包進一個Jar包。
* 在這個Jar包中要包含一個文件夾META-INF/services
* 在這個文件夾中要包含一個名爲javax.annotation.processing.Processor的文件。
* 在這個文件裏面將Jar包中所有註解處理器的完整類名寫進去,每個類名一行。

Java編譯器或者其它工具將會在所有聲明的classpath路徑下查找這個文件,並用於註冊處理器。

在我們的示例中,項目文件夾結構以及文件的內容如下:

註解處理器項目結構

註解處理器meta文件

完成打包後,我們就開始可以使用它了。


通過javac運行處理器


假設你有一個Java項目需要使用到一些自定義的註解,並且還有一些可用的註解處理器。在Java 5版本下,編譯和處理註解是兩個不同的步驟(並且也由兩個不同的工具完成),不過從Java 6開始這兩個任務都被集成到Java編譯工具javac中了。

如果你將註解處理器添加到javac的classpath下,並且使用上面提到的服務機制進行註冊了,那麼它們將會被javac調用執行。

在我們的的示例中,下面的命令將會編譯並處理使用了Complexity註解的Java源文件(譯註:-cp是javac用於指定classpath的可用選項,下面命令中將包含自定義註解和註解處理器的兩個jar包添加到classpath下):

>javac -cp sdc.assets.annotations-1.0-SNAPSHOT.jar;
     sdc.assets.annotations.processors-1.0-SNAPSHOT.jar
     SimpleAnnotationsTest.java

其中用於測試的Java類內容如下:

package sdc.startupassets.annotations.base.client;

import ...

@Complexity(ComplexityLevel.VERY_SIMPLE)
public class SimpleAnnotationsTest {

    public SimpleAnnotationsTest() {
        super();
    }

    @Complexity() // this annotation type applies also to methods
                  // the default value 'ComplexityLevel.MEDIUM' is assumed
    public void theMethod() {
        System.out.println("consoleut");
    }
}

當我們執行完javac命令後,輸出的結果如下:

javac command output

儘管使用默認的javac參數配置通常都沒有什麼問題,不過在某些情況下還有一些選項可以幫助我們運行註解處理器:

  • -Akey[=value]:用於傳遞一個選項給處理器。只有在註解處理器上通過SupportedOptions註解註冊過的選項才能傳遞進去。
  • -proc:{none|only}:默認情況下,javac會運行註解處理器並編譯全部源文件。如果使用了proc:none選項,那麼所有的註解處理過程都不會被執行——這在編譯註解處理器本身的時候很有用。如果使用了proc:only選項,則只有註解處理過程會被執行——在運行類似質量檢查工具或者標準檢查器這樣功能的註解處理器內部驗證時很有用。
  • -processorpath path:用於指定註解處理器及相關依賴的位置。這是個可選項,如果沒有設定,編譯器會去默認的classpath路徑下搜索。這在項目依賴和註解處理器依賴(運行過程中不需要的部分)之間保持明確的分離時非常有用。
  • -s dir:用於指定生成代碼的保存位置。儘管對應源代碼包層級結構的子目錄在需要的時候會被自動創建,但是指定的這個父目錄必須要在運行javac命令之前就已經存在。
  • -processor class1[,class2,class3…]:指定需要執行的註解處理器的完整類名。如果指定這個選項,上面提到的基於服務機制來搜索註解處理器過程就會被忽略。這對於在classpath下有多個註冊的註解處理器但只想執行其中幾個的情況非常有用。


通過Eclipse運行處理器


Eclipse或者其它主流IDE都能支持註解處理器並且將它們整合到常規的構建過程中。

在Eclipse下,當你打開Java項目的properties對話框後,你可以在Java Compiler分組中找到Annotation Processing選項。

annotation processing dialog

在Annotation Processing選項標籤頁中,勾選Annotation Processing選項(默認不勾選)。處理器選項參數可以通過這個標籤頁中的表單(在編譯的時候)傳遞過去。

同樣,在Factory Path選項頁中選擇需要執行的註解處理器:

Factory Path

一旦完成配置,每次編譯這個項目的時候,註冊的註解處理器都會被執行。


通過Maven運行處理器


註解處理器同樣可以被集成到Apache Maven構建工具中執行。

(在Maven中),我們能夠訪問到的自動化(處理)層級允許我們將所有的任務類型無縫集成到構建過程中。在一個項目的週期裏面,標準驗證和代碼生成不再需要一個單獨的進程。另外,它同樣允許與持續集成引擎無縫集成。

儘管還有其它集成註解處理器到Mavan構建中的方法,但是我們建議使用這裏討論的方法。這種方法基於相同的Mojo(Maven plug-in,譯註:Mojo表示一個處理過程的具體實現,通常由maven的Plugin提供這個具體過程),它能夠管理編譯的任務。實際上,由於(實際)使用了Java編譯工具,這種方法能夠產生我們熟知的標準做法。

與Maven集成需要同時將我們的註解和註解處理器以Maven artifact的形式引入。

我們建議將註解和註解處理器保存到不同的artifact中,因爲在一些客戶項目中可能不需要這些註解處理器,這樣才能減少項目的依賴數量。

在這個方法中,我們將會創建三個不同的項目,每個項目對應一個Maven artifact:

  • 關於註解的artifact。它只包含了自定義的註解類型。
  • 關於註解處理器的artifact。它包含了註解處理器。取決於這些處理器在完成它們的處理任務時需要使用的自定義註解,而添加上包含對應註解的artifact依賴。編譯插件需要配置上proc:none選項,這樣在編譯這個artifact的時候,註解處理過程纔不會被執行。
  • 客戶項目。它包含了具體的業務代碼,需要依賴前面的兩個artifact。

下面是包含註解的artifact項目文件夾結構以及POM內容:

annotation artifact dir

annotation artifact pom

注意構建上面這個artifact的maven編譯插件版本號。

下面是包含註解處理器的artifact項目文件夾結構和POM內容:

annotation processor artifact dir

annotation processor artifact pom

注意打包處理器過程中需要的services文件夾、Maven編譯插件的版本、用於在編譯這個artifact過程中阻止運行處理器的選項proc:none,以及包含註解類型的artifact的依賴。

最後,下面是客戶項目的文件夾結構和POM內容:

client artifact dir

client artifact pom

注意這裏需要依賴包含自定義註解的artifact以及包含註解處理器的artifact。

完成之後,每次執行maven構建工具的compile目標,註解處理器都會按照預設的執行。


本系列將會在第三部分中繼續講解:生成源代碼。請閱讀這裏

[1]:從這裏查看Class接口文檔獲取更多內容
[2]:JSR269,動態註解處理器API,可以在這裏在線查看。


譯附:
譯者根據本文整理並修改的maven demo項目:
Github地址:https://github.com/qinxiandiqi/AnnotationProcessorDemo
Tag標籤:part2(執行git checkout part2命令檢出)

發佈了69 篇原創文章 · 獲贊 101 · 訪問量 63萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章