1. 一些基本概念
在開始之前,我們需要聲明一件重要的事情是:我們不是在討論在運行時通過反射機制運行處理的注解,而是在討論在編譯時處理的注解。
注解處理器是 javac 自帶的一個工具,用來在編譯時期掃描處理注解信息。你可以為某些注解注冊自己的注解處理器。這里,我假設你已經了解什么是注解及如何自定義注解。如果你還未了解注解的話,可以查看官方文檔。注解處理器在 Java 5 的時候就已經存在了,但直到 Java 6 (發布於2006看十二月)的時候才有可用的API。過了一段時間java的使用者們才意識到注解處理器的強大。所以最近幾年它才開始流行。
一個特定注解的處理器以 java 源代碼(或者已編譯的字節碼)作為輸入,然后生成一些文件(通常是.java
文件)作為輸出。那意味着什么呢?你可以生成 java 代碼!這些 java 代碼在生成的.java
文件中。因此你不能改變已經存在的java類,例如添加一個方法。這些生成的 java 文件跟其他手動編寫的 java 源代碼一樣,將會被 javac 編譯。
Annotation processing是在編譯階段執行的,它的原理就是讀入Java源代碼,解析注解,然后生成新的Java代碼。新生成的Java代碼最后被編譯成Java字節碼,注解解析器(Annotation Processor)不能改變讀入的Java 類,比如不能加入或刪除Java方法。
2. AbstractProcessor
讓我們來看一下處理器的 API。所有的處理器都繼承了AbstractProcessor
,如下所示:
1 package com.example; 2 3 import java.util.LinkedHashSet; 4 import java.util.Set; 5 import javax.annotation.processing.AbstractProcessor; 6 import javax.annotation.processing.ProcessingEnvironment; 7 import javax.annotation.processing.RoundEnvironment; 8 import javax.annotation.processing.SupportedAnnotationTypes; 9 import javax.annotation.processing.SupportedSourceVersion; 10 import javax.lang.model.SourceVersion; 11 import javax.lang.model.element.TypeElement; 12 13 public class MyProcessor extends AbstractProcessor { 14 15 @Override 16 public boolean process(Set<? extends TypeElement> annoations, 17 RoundEnvironment env) { 18 return false; 19 } 20 21 @Override 22 public Set<String> getSupportedAnnotationTypes() { 23 Set<String> annotataions = new LinkedHashSet<String>(); 24 annotataions.add("com.example.MyAnnotation"); 25 return annotataions; 26 } 27 28 @Override 29 public SourceVersion getSupportedSourceVersion() { 30 return SourceVersion.latestSupported(); 31 } 32 33 @Override 34 public synchronized void init(ProcessingEnvironment processingEnv) { 35 super.init(processingEnv); 36 } 37 38 }
-
init(ProcessingEnvironment processingEnv)
:所有的注解處理器類都必須有一個無參構造函數。然而,有一個特殊的方法init()
,它會被注解處理工具調用,以ProcessingEnvironment
作為參數。ProcessingEnvironment 提供了一些實用的工具類Elements
,Types
和Filer
。我們在后面將會使用到它們。 -
process(Set<? extends TypeElement> annoations, RoundEnvironment env)
:這類似於每個處理器的main()
方法。你可以在這個方法里面編碼實現掃描,處理注解,生成 java 文件。使用RoundEnvironment
參數,你可以查詢被特定注解標注的元素(原文:you can query for elements annotated with a certain annotation )。后面我們將會看到詳細內容。 -
getSupportedAnnotationTypes()
:在這個方法里面你必須指定哪些注解應該被注解處理器注冊。注意,它的返回值是一個String
集合,包含了你的注解處理器想要處理的注解類型的全稱。換句話說,你在這里定義你的注解處理器要處理哪些注解。 -
getSupportedSourceVersion()
: 用來指定你使用的 java 版本。通常你應該返回SourceVersion.latestSupported()
。不過,如果你有足夠的理由堅持用 java 6 的話,你也可以返回SourceVersion.RELEASE_6
。我建議使用SourceVersion.latestSupported()
。在 Java 7 中,你也可以使用注解的方式來替代重寫getSupportedAnnotationTypes()
和getSupportedSourceVersion()
,如下所示:
@SupportedSourceVersion(value=SourceVersion.RELEASE_7) @SupportedAnnotationTypes({ // Set of full qullified annotation type names "com.example.MyAnnotation", "com.example.AnotherAnnotation" }) public class MyProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { return false; } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } }
由於兼容性問題,特別是對於 android ,我建議重寫getSupportedAnnotationTypes()
和 getSupportedSourceVersion()
,而不是使用 @SupportedAnnotationTypes
和 @SupportedSourceVersion
。
接下來你必須知道的事情是:注解處理器運行在它自己的 JVM 中。是的,你沒看錯。javac 啟動了一個完整的 java 虛擬機來運行注解處理器。這意味着什么?你可以使用任何你在普通 java 程序中使用的東西。使用 guava
! 你可以使用依賴注入工具,比如dagger
或者任何其他你想使用的類庫。但不要忘記,即使只是一個小小的處理器,你也應該注意使用高效的算法及設計模式,就像你在開發其他 java 程序中所做的一樣。
3. 注冊你的處理器
你可能會問 “怎樣注冊我的注解處理器到 javac ?”。你必須提供一個.jar
文件。就像其他 .jar 文件一樣,你將你已經編譯好的注解處理器打包到此文件中。並且,在你的 .jar 文件中,你必須打包一個特殊的文件javax.annotation.processing.Processor
到META-INF/services
目錄下。因此你的 .jar 文件目錄結構看起來就你這樣:
MyProcess.jar -com -example -MyProcess.class -META-INF -services -javax.annotation.processing.Processor
javax.annotation.processing.Processor
文件的內容是一個列表,每一行是一個注解處理器的全稱。例如:
com.example.MyProcess com.example.AnotherProcess
4. 例子:工廠模式
我們要解決的問題是:我們要實現一個 pizza 店,這個 pizza 店提供給顧客兩種 pizza (Margherita 和 Calzone),還有甜點 Tiramisu(提拉米蘇)。
1 public interface Meal { 2 public float getPrice(); 3 } 4 public class MargheritaPizza implements Meal{ 5 @Override 6 public float getPrice() { 7 return 6.0f; 8 } 9 } 10 public class CalzonePizza implements Meal{ 11 @Override 12 public float getPrice() { 13 return 8.5f; 14 } 15 } 16 public class Tiramisu implements Meal{ 17 @Override 18 public float getPrice() { 19 return 4.5f; 20 } 21 } 22 23 public class PizzaStore { 24 25 public Meal order(String mealName) { 26 if (null == mealName) { 27 throw new IllegalArgumentException("name of meal is null!"); 28 } 29 if ("Margherita".equals(mealName)) { 30 return new MargheritaPizza(); 31 } 32 33 if ("Calzone".equals(mealName)) { 34 return new CalzonePizza(); 35 } 36 37 if ("Tiramisu".equals(mealName)) { 38 return new Tiramisu(); 39 } 40 41 throw new IllegalArgumentException("Unknown meal '" + mealName + "'"); 42 } 43 44 private static String readConsole() { 45 Scanner scanner = new Scanner(System.in); 46 String meal = scanner.nextLine(); 47 scanner.close(); 48 return meal; 49 } 50 51 public static void main(String[] args) { 52 System.out.println("welcome to pizza store"); 53 PizzaStore pizzaStore = new PizzaStore(); 54 Meal meal = pizzaStore.order(readConsole()); 55 System.out.println("Bill:$" + meal.getPrice()); 56 } 57 }
正如你所見,在order()
方法中,我們有許多 if 條件判斷語句。並且,如果我們添加一種新的 pizza 的話,我們就得添加一個新的 if 條件判斷。但是等一下,使用注解處理器和工廠模式,我們可以讓一個注解處理器生成這些 if 語句。如此一來,我們想要的代碼就像這樣子:
1 public class PizzaStore { 2 3 private MealFactory factory = new MealFactory(); 4 5 public Meal order(String mealName) { 6 return factory.create(mealName); 7 } 8 9 private static String readConsole() { 10 Scanner scanner = new Scanner(System.in); 11 String meal = scanner.nextLine(); 12 scanner.close(); 13 return meal; 14 } 15 16 public static void main(String[] args) { 17 System.out.println("welcome to pizza store"); 18 PizzaStore pizzaStore = new PizzaStore(); 19 Meal meal = pizzaStore.order(readConsole()); 20 System.out.println("Bill:$" + meal.getPrice()); 21 } 22 } 23 24 public class MealFactory { 25 26 public Meal create(String id) { 27 if (id == null) { 28 throw new IllegalArgumentException("id is null!"); 29 } 30 if ("Calzone".equals(id)) { 31 return new CalzonePizza(); 32 } 33 34 if ("Tiramisu".equals(id)) { 35 return new Tiramisu(); 36 } 37 38 if ("Margherita".equals(id)) { 39 return new MargheritaPizza(); 40 } 41 42 throw new IllegalArgumentException("Unknown id = " + id); 43 } 44 }
5. @Factory Annotation
能猜到么,我們打算使用注解處理器生成MealFactory
類。更一般的說,我們想要提供一個注解和一個處理器用來生成工廠類。
讓我們看一下@Factory
注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Factory { /** * The name of the factory */ Class<?> type(); /** * The identifier for determining which item should be instantiated */ String id(); }
思想是這樣的:我們注解那些食物類,使用type()
表示這個類屬於哪個工廠,使用id()
表示這個類的具體類型。讓我們將@Factory
注解應用到這些類上吧:
1 @Factory(type=MargheritaPizza.class, id="Margherita") 2 public class MargheritaPizza implements Meal{ 3 4 @Override 5 public float getPrice() { 6 return 6.0f; 7 } 8 } 9 10 @Factory(type=CalzonePizza.class, id="Calzone") 11 public class CalzonePizza implements Meal{ 12 13 @Override 14 public float getPrice() { 15 return 8.5f; 16 } 17 } 18 19 @Factory(type=Tiramisu.class, id="Tiramisu") 20 public class Tiramisu implements Meal{ 21 22 @Override 23 public float getPrice() { 24 return 4.5f; 25 } 26 }
你可能會問,我們是不是可以只將@Factory
注解應用到Meal
接口上?答案是不行,因為注解是不能被繼承的。即在class X
上有注解,class Y extends X
,那么class Y
是不會繼承class X
上的注解的。在我們編寫處理器之前,需要明確幾點規則:
- 只有類能夠被
@Factory
注解,因為接口和虛類是不能通過new
操作符實例化的。 - 被
@Factory
注解的類必須提供一個默認的無參構造函數。否則,我們不能實例化一個對象。 - 被
@Factory
注解的類必須直接繼承或者間接繼承type
指定的類型。(或者實現它,如果type
指定的是一個接口) - 被
@Factory
注解的類中,具有相同的type
類型的話,這些類就會被組織起來生成一個工廠類。工廠類以Factory
作為后綴,例如:type=Meal.class
將會生成MealFactory
類。 id
的值只能是字符串,且在它的type
組中必須是唯一的。
注解處理器:
1 public class FactoryProcessor extends AbstractProcessor { 2 3 private Types typeUtils; 4 private Elements elementUtils; 5 private Filer filer; 6 private Messager messager; 7 private Map<String, FactoryGroupedClasses> factoryClasses = 8 new LinkedHashMap<String, FactoryGroupedClasses>(); 9 10 @Override 11 public synchronized void init(ProcessingEnvironment processingEnv) { 12 super.init(processingEnv); 13 typeUtils = processingEnv.getTypeUtils(); 14 elementUtils = processingEnv.getElementUtils(); 15 filer = processingEnv.getFiler(); 16 messager = processingEnv.getMessager(); 17 } 18 19 @Override 20 public boolean process(Set<? extends TypeElement> arg0, 21 RoundEnvironment arg1) { 22 ... 23 return false; 24 } 25 26 @Override 27 public Set<String> getSupportedAnnotationTypes() { 28 Set<String> annotataions = new LinkedHashSet<String>(); 29 annotataions.add(Factory.class.getCanonicalName()); 30 return annotataions; 31 } 32 33 @Override 34 public SourceVersion getSupportedSourceVersion() { 35 return SourceVersion.latestSupported(); 36 } 37 }
在getSupportedAnnotationTypes()
方法中,我們指定@Factory
注解將被這個處理器處理。