Java注解處理器--編譯時處理的注解


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 提供了一些實用的工具類ElementsTypesFiler。我們在后面將會使用到它們。

  • 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.ProcessorMETA-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上的注解的。在我們編寫處理器之前,需要明確幾點規則:

  1. 只有類能夠被@Factory注解,因為接口和虛類是不能通過new操作符實例化的。
  2. @Factory注解的類必須提供一個默認的無參構造函數。否則,我們不能實例化一個對象。
  3. @Factory注解的類必須直接繼承或者間接繼承type指定的類型。(或者實現它,如果type指定的是一個接口)
  4. @Factory注解的類中,具有相同的type類型的話,這些類就會被組織起來生成一個工廠類。工廠類以Factory作為后綴,例如:type=Meal.class將會生成MealFactory類。
  5. 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注解將被這個處理器處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM