Annotation-Processing-Tool詳解

ShellaKowal 8年前發布 | 20K 次閱讀 Java Java開發

在這篇文章中我將闡述如何實現一個注解處理器。首先我將向你解釋什么是注解處理器,你可以使用這個強大的工具來做什么及不能做什么。接下來我們將一步一步來實現一個簡單的注解處理器。

1. 一些基本概念

在開始之前,我們需要聲明一件重要的事情是:我們不是在討論在運行時通過反射機制運行處理的注解,而是在討論在編譯時處理的注解。
注解處理器是 javac 自帶的一個工具,用來在編譯時期掃描處理注解信息。你可以為某些注解注冊自己的注解處理器。這里,我假設你已經了解什么是注解及如何自定義注解。如果你還未了解注解的話,可以查看官方文檔。注解處理器在 Java 5 的時候就已經存在了,但直到 Java 6 (發布于2006看十二月)的時候才有可用的API。過了一段時間java的使用者們才意識到注解處理器的強大。所以最近幾年它才開始流行。
一個特定注解的處理器以 java 源代碼(或者已編譯的字節碼)作為輸入,然后生成一些文件(通常是.java文件)作為輸出。那意味著什么呢?你可以生成 java 代碼!這些 java 代碼在生成的.java文件中。因此你不能改變已經存在的java類,例如添加一個方法。這些生成的 java 文件跟其他手動編寫的 java 源代碼一樣,將會被 javac 編譯。

2. AbstractProcessor

讓我們來看一下處理器的 API。所有的處理器都繼承了AbstractProcessor,如下所示:

package com.example;

import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
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;

public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add("com.example.MyAnnotation");
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

}
  • 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. 例子:工廠模式

現在可以舉一個實際的例子了。我們使用maven 工具來作為我們的編譯系統和依賴管理工具。我會把例子的代碼放到 github上。
首先,我必須要說的是,想要找到一個可以使用注解處理器去解決的簡單問題來當作教程,并不是一件容易的事。這篇教程中,我們將實現一個非常簡單的工廠模式(不是抽象工廠模式)。它只是為了給你簡明的介紹注解處理器的API而已。所以這個問題的程序,并不是那么有用,也不是一個真實開發中的例子。再次聲明,你能學到的只是注解處理器的相關內容,而不是設計模式。

我們要解決的問題是:我們要實現一個 pizza 店,這個 pizza 店提供給顧客兩種 pizza (Margherita 和 Calzone),還有甜點 Tiramisu(提拉米蘇)。
簡單看一下這段代碼:
Meal.java

package com.example.pizza;

public interface Meal {
    public float getPrice();
}

MargheritaPizza.java

package com.example.pizza;

public class MargheritaPizza implements Meal{
    @Override
    public float getPrice() {
        return 6.0f;
    }
}

CalzonePizza.java

package com.example.pizza;

public class CalzonePizza implements Meal{
    @Override
    public float getPrice() {
        return 8.5f;
    }
}

Tiramisu.java

package com.example.pizza;

public class Tiramisu implements Meal{
    @Override
    public float getPrice() {
        return 4.5f;
    }
}

顧客要在我們的 pizza 店購買食物的話,就得輸入食物的名稱:
PizzaStore.java

package com.example.pizza;

import java.util.Scanner;

public class PizzaStore {

    public Meal order(String mealName) {
        if (null == mealName) {
            throw new IllegalArgumentException("name of meal is null!");
        }
        if ("Margherita".equals(mealName)) {
            return new MargheritaPizza();
        }

        if ("Calzone".equals(mealName)) {
            return new CalzonePizza();
        }

        if ("Tiramisu".equals(mealName)) {
            return new Tiramisu();
        }

        throw new IllegalArgumentException("Unknown meal '" + mealName + "'");
    }

    private static String readConsole() {
        Scanner scanner = new Scanner(System.in);
        String meal = scanner.nextLine();
        scanner.close();
        return meal;
    }

    public static void main(String[] args) {
        System.out.println("welcome to pizza store");
        PizzaStore pizzaStore = new PizzaStore();
        Meal meal = pizzaStore.order(readConsole());
        System.out.println("Bill:$" + meal.getPrice());
    }
}

正如你所見,在order()方法中,我們有許多 if 條件判斷語句。并且,如果我們添加一種新的 pizza 的話,我們就得添加一個新的 if 條件判斷。但是等一下,使用注解處理器和工廠模式,我們可以讓一個注解處理器生成這些 if 語句。如此一來,我們想要的代碼就像這樣子:
PizzaStore.java

package com.example.pizza;

import java.util.Scanner;

public class PizzaStore {

    private MealFactory factory = new MealFactory();

    public Meal order(String mealName) {
        return factory.create(mealName);
    }

    private static String readConsole() {
        Scanner scanner = new Scanner(System.in);
        String meal = scanner.nextLine();
        scanner.close();
        return meal;
    }

    public static void main(String[] args) {
        System.out.println("welcome to pizza store");
        PizzaStore pizzaStore = new PizzaStore();
        Meal meal = pizzaStore.order(readConsole());
        System.out.println("Bill:$" + meal.getPrice());
    }
}

MealFactory 類應該是這樣的:
MealFactory.java

package com.example.pizza;

public class MealFactory {

    public Meal create(String id) {
        if (id == null) {
            throw new IllegalArgumentException("id is null!");
        }
        if ("Calzone".equals(id)) {
            return new CalzonePizza();
        }

        if ("Tiramisu".equals(id)) {
            return new Tiramisu();
        }

        if ("Margherita".equals(id)) {
            return new MargheritaPizza();
        }

        throw new IllegalArgumentException("Unknown id = " + id);
    }
}

5. @Factory Annotation

能猜到么,我們打算使用注解處理器生成MealFactory類。更一般的說,我們想要提供一個注解和一個處理器用來生成工廠類。
讓我們看一下@Factory注解:
Factory.java

package com.example.apt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@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注解應用到這些類上吧:
MargheritaPizza.java

package com.example.pizza;

import com.example.apt.Factory;

@Factory(type=MargheritaPizza.class, id="Margherita")
public class MargheritaPizza implements Meal{

    @Override
    public float getPrice() {
        return 6.0f;
    }
}

CalzonePizza.java

package com.example.pizza;

import com.example.apt.Factory;

@Factory(type=CalzonePizza.class, id="Calzone")
public class CalzonePizza implements Meal{

    @Override
    public float getPrice() {
        return 8.5f;
    }
}

Tiramisu.java

package com.example.pizza;

import com.example.apt.Factory;

@Factory(type=Tiramisu.class, id="Tiramisu")
public class Tiramisu implements Meal{

    @Override
    public float getPrice() {
        return 4.5f;
    }
}

 

 

 

你可能會問,我們是不是可以只將@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組中必須是唯一的。

6. 注解處理器

我將會通過添加一段代碼接著解釋這段代碼的方法,一步一步引導你。三個點號(...)表示省略那部分前面已經討論過或者將在后面討論的代碼。目的就是為了讓代碼片段更具有可讀性。前面已經說過,我們的完整代碼將放到github上。OK,讓我們開始編寫我們的FactoryProcessor的框架吧:
FactoryProcessor.java

package com.example.apt;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

public class FactoryProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;
    private Map<String, FactoryGroupedClasses> factoryClasses = 
            new LinkedHashMap<String, FactoryGroupedClasses>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> arg0,
            RoundEnvironment arg1) {
        ...
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(Factory.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

getSupportedAnnotationTypes()方法中,我們指定@Factory注解將被這個處理器處理。

7. Elements and TypeMirrors

init()方法中,我們使用了以下類型:

  • Elements:一個用來處理Element的工具類(后面詳細說明)
  • Types:一個用來處理TypeMirror的工具類(后面詳細說明)
  • Filer:正如這個類的名字所示,你可以使用這個類來創建文件

在注解處理器中,我們掃描 java 源文件,源代碼中的每一部分都是Element的一個特定類型。換句話說:Element代表程序中的元素,比如說 包,類,方法。每一個元素代表一個靜態的,語言級別的結構。在下面的例子中,我將添加注釋來說明這個問題:

package com.example;

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

你得換個角度來看源代碼。它只是結構化的文本而已。它不是可以執行的。你可以把它當作 你試圖去解析的XML 文件。或者一棵編譯中創建的抽象語法樹。就像在 XML 解析器中,有許多DOM元素。你可以通過一個元素找到它的父元素或者子元素。
例如:如果你有一個代表public class FooTypeElement,你就可以迭代訪問它的子結點:

TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // iterate over children
    Element parent = e.getEnclosingElement();  // parent == fooClass
}

如你所見,Elements代表源代碼,TypeElement代表源代碼中的元素類型,例如類。然后,TypeElement并不包含類的相關信息。你可以從TypeElement獲取類的名稱,但你不能獲取類的信息,比如說父類。這些信息可以通過TypeMirror獲取。你可以通過調用element.asType()來獲取一個ElementTypeMirror

(譯注:關于getEnclosedElementsgetEnclosingElement 的解釋,參見官方文檔

8. Searching For @Factory

讓我們一步一步來實現process()方法吧。首先我們掃描所有被@Factory注解的類:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
        ...
    }
    return false;
}

這里并沒有什么高深的技術。roundEnv.getElementsAnnotatedWith(Factory.class) 返回一個被@Factory注解的元素列表。你可能注意到我避免說“返回一個被@Factory注解的類列表”。因為它的確是返回了一個Element列表。記住:Element可以是類,方法,變量等。所以,我們下一步需要做的是檢查這個元素是否是一個類:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
        if(annotatedElement.getKind() != ElementKind.CLASS) {
            ...     
            }
    }
    return false;
}

 

 

 

為什么需要這樣做呢?因為我們要確保只有class類型的元素被我們的處理器處理。前面我們已經學過,類是一種TypeElement元素。那我們為什么不使用if (! (annotatedElement instanceof TypeElement))來檢查呢?這是錯誤的判斷,因為接口也是一種TypeElement類型。所以在注解處理器中,你應該避免使用instanceof,應該用ElementKind或者配合TypeMirror使用TypeKind

9. 錯誤處理

init()方法中,我們也獲取了一個Messager的引用。Messager為注解處理器提供了一種報告錯誤消息,警告信息和其他消息的方式。它不是注解處理器開發者的日志工具。Messager是用來給那些使用了你的注解處理器的第三方開發者顯示信息的。在官方文檔中描述了不同級別的信息。非常重要的是Kind.ERROR,因為這種消息類型是用來表明我們的注解處理器在處理過程中出錯了。有可能是第三方開發者誤使用了我們的@Factory注解(比如,使用@Factory注解了一個接口)。這個概念與傳統的 java 應用程序有一點區別。傳統的 java 應用程序出現了錯誤,你可以拋出一個異常。如果你在process()中拋出了一個異常,那 jvm 就會崩潰。注解處理器的使用者將會得到一個從 javac 給出的非常難懂的異常錯誤信息。因為它包含了注解處理器的堆棧信息。因此注解處理器提供了Messager類。它能打印漂亮的錯誤信息,而且你可以鏈接到引起這個錯誤的元素上。在現代的IDE中,第三方開發者可以點擊錯誤信息,IDE會跳轉到產生錯誤的代碼行中,以便快速定位錯誤。
回到process()方法的實現。如果用戶將@Factory注解到了一個非class的元素上,我們就拋出一個錯誤信息:

@Override
public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
        if(annotatedElement.getKind() != ElementKind.CLASS) {
            error(annotatedElement, "Only classes can be annotated with @%s",
                    Factory.class.getSimpleName());
            return true; // Exit processing
        }
    }
    return false;
}

private void error(Element e, String msg, Object... args) {
    messager.printMessage(
        Diagnostic.Kind.ERROR,
        String.format(msg, args),
        e);
  }

為了能夠獲取Messager顯示的信息,非常重要的是注解處理器必須不崩潰地完成運行。這就是我們在調用error()后執行return true的原因。如果我們在這里沒有返回的話,process()就會繼續運行,因為messager.printMessage( Diagnostic.Kind.ERROR)并不會終止進程。如果我們沒有在打印完錯誤信息后返回的話,我們就可能會運行到一個空指針異常等等。就像前面所說的,如果我們繼續運行process(),一旦有處理的異常在process()中被拋出,javac 就會打印注解處理器的空指針異常堆棧信息,而不是Messager顯示的信息。

10. 數據模型

在我們繼續檢查被@Factory注解的類是否滿足我們前面所說的五條規則之前,我們先介紹一個數據結構,它能讓我們更方便的繼續處理接下來的工作。有時候問題或者處理器看起來太過簡單了,導致一些程序員傾向于用面向過程的方法編寫整個處理器。但你知道嗎?一個注解處理器仍然是一個 java 程序。所以我們應該使用面向對象,接口,設計模式以及任何你可能在其他普通Java程序中使用的技巧。
雖然我們的FactoryProcessor非常簡單,但是我們仍然想將一些信息作為對象保存。在FactoryAnnotationClass中,我們保存被注解的類的數據,比如合法的類名以及@Factory注解本身的一些數據。所以,我們保存TypeElement和處理過的@Factory注解:
FactoryAnnotationClass.java

package com.example.apt;

import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;

public class FactoryAnnotatedClass {

    private TypeElement annotatedClassElement;
    private String qualifiedSuperClassName;
    private String simpleTypeName;
    private String id;

    public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
        this.annotatedClassElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        id = annotation.id();

        if ("".equals(id)) {
            throw new IllegalArgumentException(
                    String.format(
                            "id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), 
                            classElement.getQualifiedName().toString()));
        }

        // Get the full QualifiedTypeName
        try {
            Class<?> clazz = annotation.type();
            qualifiedSuperClassName = clazz.getCanonicalName();
            simpleTypeName = clazz.getSimpleName();
        } catch (MirroredTypeException mte) {
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
            simpleTypeName = classTypeElement.getSimpleName().toString();
        }
    }

    /**
     * Get the id as specified in {@link Factory#id()}. return the id
     */
    public String getId() {
        return id;
    }

    /**
     * Get the full qualified name of the type specified in
     * {@link Factory#type()}.
     * 
     * @return qualified name
     */
    public String getQualifiedFactoryGroupName() {
        return qualifiedSuperClassName;
    }

    /**
     * Get the simple name of the type specified in {@link Factory#type()}.
     * 
     * @return qualified name
     */
    public String getSimpleFactoryGroupName() {
        return simpleTypeName;
    }

    /**
     * The original element that was annotated with @Factory
     */
    public TypeElement getTypeElement() {
        return annotatedClassElement;
    }
}

看起來有很多代碼,但是最重要的代碼在構造函數中,你可以看到如下的代碼:

Factory annotation = classElement.getAnnotation(Factory.class);
        id = annotation.id();

        if ("".equals(id)) {
            throw new IllegalArgumentException(
                    String.format(
                            "id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), 
                            classElement.getQualifiedName().toString()));
        }

這里,我們獲取@Factory注解,并檢查id是否為空。如果id為空,我們將會拋出一個IllegalArgumentException異常。你可能感到疑惑的是,前面我們說了不要拋出異常,而是使用Messager。但這并不矛盾。我們在這里拋出一個內部異常,后面你將會看到我們在process()中捕獲了這個異常。我們有兩個這樣做的理由:

  1. 我想說明你應該仍然像普通的Java程序一樣編碼。拋出和捕獲異常被認為是一個好的Java編程實踐。

  2. 如果我們想要在FactoryAnnotatedClass中正確地打印信息,我們也需要傳入Messager對象,就像我們在錯誤處理一節中已經提到的,注解處理器必須成功結束,才能讓Messager打印錯誤信息。如果我們想使用Messager打印一個錯誤信息,我們應該怎樣通知process()發生了一個錯誤?最簡單的方法,并且我認為了直觀的方法,就是拋出一個異常然后讓步process()捕獲它。

接下來,我們將獲取@Fractory注解中的type成員域。我們比較感興趣的是合法的全名:

// Get the full QualifiedTypeName
try {
    Class<?> clazz = annotation.type();
    qualifiedSuperClassName = clazz.getCanonicalName();
    simpleTypeName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {
    DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
    TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
    qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
    simpleTypeName = classTypeElement.getSimpleName().toString();
}

這有點棘手,因為這里的類型是java.lang.Class。那意味著,這是一個真實的Class對象。因為注解處理器在編譯 java 源碼之前執行,所以我們必須得考慮兩種情況:

  1. 這個類已經被編譯過了:這種情況是第三方 .jar 包含已編譯的被@Factory注解 .class 文件。這種情況下,我們可以像try 代碼塊中所示那樣直接獲取Class。 (譯注:因為@Factory@RetentionRetentionPolicy.CLASS,所有被編譯過的代碼也會保留@Factory的注解信息)
  2. 這個類還沒有被編譯:這種情況是我們嘗試編譯被@Fractory注解的源代碼。這種情況下,直接獲取Class會拋出MirroredTypeException異常。幸運的是,MirroredTypeException包含一個TypeMirror,它表示我們未被編譯類。因為我們知道它一定是一個Class類型(我們前面有檢查過),所以我們可以將它轉換為DeclaredType, 然后獲取TypeElement來讀取合法名稱。

好了,我們還需要一個叫FactoryGroupedClasses的數據結構,用來簡單的組合所有的FactoryAnnotatedClasses到一起。

package com.example.apt;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.annotation.processing.Filer;
import javax.lang.model.util.Elements;

public class FactoryGroupedClasses {

    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert)
            throws IdAlreadyUsedException {

        FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
        if (existing != null) {
            throw new IdAlreadyUsedException(existing);
        }

        itemsMap.put(toInsert.getId(), toInsert);
    }

    public void generateCode(Elements elementUtils, Filer filer)
            throws IOException {
        ...
    }
}

如你所見,它只是一個基本的Map<String, FactoryAnnotatedClass>,這個map用來映射@Factory.id()FactoryAnnotatedClass。我們選擇使用Map,是因為我們想確保每一個id都的唯一的。使用map查找,這可以很容易實現。generateCode()將會被調用來生成工廠在的代碼(稍后討論)。

11. 匹配準則

讓我們來繼續實現process()方法。接下來我們要檢查被注解的類至少有一個公有構造函數,不是抽象類,繼承了特定的類,以及是一個public類:

@Override
public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv
            .getElementsAnnotatedWith(Factory.class)) {
        if (annotatedElement.getKind() != ElementKind.CLASS) {
            error(annotatedElement,
                    "Only classes can be annotated with @%s",
                    Factory.class.getSimpleName());
            return true; // Exit processing
        }

        // We can cast it, because we know that it of ElementKind.CLASS
        TypeElement typeElement = (TypeElement) annotatedElement;

        try {
            FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(
                    typeElement); // throws IllegalArgumentException

            if (!isValidClass(annotatedClass)) {
                return true; // Error message printed, exit processing
            }
        } catch (IllegalArgumentException e) {
            // @Factory.id() is empty
            error(typeElement, e.getMessage());
            return true;
        }
        ...
    }
    return false;
}

private boolean isValidClass(FactoryAnnotatedClass item) {

    // Cast to TypeElement, has more type specific methods
    TypeElement classElement = item.getTypeElement();

    if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
        error(classElement, "The class %s is not public.", classElement
                .getQualifiedName().toString());
        return false;
    }

    // Check if it's an abstract class
    if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {
        error(classElement,
                "The class %s is abstract. You can't annotate abstract classes with @%",
                classElement.getQualifiedName().toString(),
                Factory.class.getSimpleName());
        return false;
    }

    // Check inheritance: Class must be childclass as specified in
    // @Factory.type();
    TypeElement superClassElement = elementUtils.getTypeElement(item
            .getQualifiedFactoryGroupName());
    if (superClassElement.getKind() == ElementKind.INTERFACE) {
        // Check interface implemented
        if (!classElement.getInterfaces().contains(
                superClassElement.asType())) {
            error(classElement,
                    "The class %s annotated with @%s must implement the interface %s",
                    classElement.getQualifiedName().toString(),
                    Factory.class.getSimpleName(),
                    item.getQualifiedFactoryGroupName());
            return false;
        }
    } else {
        // Check subclassing
        TypeElement currentClass = classElement;
        while (true) {
            TypeMirror superClassType = currentClass.getSuperclass();

            if (superClassType.getKind() == TypeKind.NONE) {
                // Basis class (java.lang.Object) reached, so exit
                error(classElement,
                        "The class %s annotated with @%s must inherit from %s",
                        classElement.getQualifiedName().toString(),
                        Factory.class.getSimpleName(),
                        item.getQualifiedFactoryGroupName());
                return false;
            }

            if (superClassType.toString().equals(
                    item.getQualifiedFactoryGroupName())) {
                // Required super class found
                break;
            }

            // Moving up in inheritance tree
            currentClass = (TypeElement) typeUtils
                    .asElement(superClassType);
        }
    }
    // Check if an empty public constructor is given
    for (Element enclosed : classElement.getEnclosedElements()) {
        if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
            ExecutableElement constructorElement = (ExecutableElement) enclosed;
            if (constructorElement.getParameters().size() == 0
                    && constructorElement.getModifiers().contains(
                            Modifier.PUBLIC)) {
                // Found an empty constructor
                return true;
            }
        }
    }

    // No empty constructor found
    error(classElement,
            "The class %s must provide an public empty default constructor",
            classElement.getQualifiedName().toString());
    return false;
}

我們添加了一個isValidClass()方法,它檢查是否我們所有的規則都被滿足了:

  • 類必須是public的:classElement.getModifiers().contains(Modifier.PUBLIC)
  • 類不能是抽象的:classElement.getModifiers().contains(Modifier.ABSTRACT)
  • 類必須是@Factoy.type()指定的類型的子類或者接口的實現:首先,我們使用elementUtils.getTypeElement(item.getQualifiedFactoryGroupName())來創建一個元素。沒錯,你可以創建一個TypeElement(使用TypeMirror),只要你知道合法的類名稱。然后我們檢查它是一個接口還是一個類:superClassElement.getKind() == ElementKind.INTERFACE。有兩種情況:如果它是一個接口,就判斷classElement.getInterfaces().contains(superClassElement.asType())。如果是類,我們就必須使用currentClass.getSuperclass()掃描繼承樹。注意,整個檢查也可以使用typeUtils.isSubtype()來實現。
  • 類必須有一個public的無參構造函數:我們遍歷所有該類直接封裝的元素classElement.getEnclosedElements(),然后檢查ElementKind.CONSTRUCTORModifier.PUBLIC 和constructorElement.getParameters().size() == 0

如果以上這些條件全都滿足,則isValidClass()返回 true,否則,它打印一個錯誤信息然后返回 false

11. 組合被注解的類

一旦我們檢查isValidClass()成功,我們就繼續添加FactoryAnnotatedClass到相應的FactoryGroupedClasses中,如下所示:

public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv
            .getElementsAnnotatedWith(Factory.class)) {
            ...
        try {
            FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(
                    typeElement); // throws IllegalArgumentException

            if (!isValidClass(annotatedClass)) {
                return true; // Error message printed, exit processing
            }

            // Everything is fine, so try to add
            FactoryGroupedClasses factoryClass = factoryClasses
                    .get(annotatedClass.getQualifiedFactoryGroupName());
            if (factoryClass == null) {
                String qualifiedGroupName = annotatedClass
                        .getQualifiedFactoryGroupName();
                factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
                factoryClasses.put(qualifiedGroupName, factoryClass);
            }

            // Throws IdAlreadyUsedException if id is conflicting with
            // another @Factory annotated class with the same id
            factoryClass.add(annotatedClass);
        } catch (IllegalArgumentException e) {
            // @Factory.id() is empty --> printing error message
            error(typeElement, e.getMessage());
            return true;
        } catch (IdAlreadyUsedException e) {
            FactoryAnnotatedClass existing = e.getExisting();
            // Already existing
            error(annotatedElement,
                    "Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id",
                    typeElement.getQualifiedName().toString(),
                    Factory.class.getSimpleName(), existing
                            .getTypeElement().getQualifiedName().toString());
            return true;
        }
    ...

12. 代碼生成

我們已經收集了所有被@Factory注解的類的信息,這些信息以FactoryAnnotatedClass的形式保存在FactoryGroupedClass中。現在我們可以為每一個工廠生成 java 文件了:

public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
        ...
    try {
        for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
            factoryClass.generateCode(elementUtils, filer);
        }
    } catch (IOException e) {
        error(null, e.getMessage());
    }

    return true;
}

寫 java 文件跟寫其他文件完全一樣。我們可以使用Filer提供的一個Writer對象來操作。我們可以用字符串拼接的方法寫入我們生成的代碼。幸運的是,Square公司(因為提供了許多非常優秀的開源項目二非常有名)給我們提供了JavaWriter,這是一個高級的生成Java代碼的庫:

package com.example.apt;

import java.io.IOException;
import java.io.Writer;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.JavaFileObject;

import com.squareup.javawriter.JavaWriter;

public class FactoryGroupedClasses {

    /**
     * Will be added to the name of the generated factory class
     */
    private static final String SUFFIX = "Factory";

    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert)
            throws IdAlreadyUsedException {

        FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
        if (existing != null) {
            throw new IdAlreadyUsedException(existing);
        }

        itemsMap.put(toInsert.getId(), toInsert);
    }

    public void generateCode(Elements elementUtils, Filer filer)
            throws IOException {
        TypeElement superClassName = elementUtils
                .getTypeElement(qualifiedClassName);
        String factoryClassName = superClassName.getSimpleName() + SUFFIX;

        JavaFileObject jfo = filer
                .createSourceFile(qualifiedClassName + SUFFIX);
        Writer writer = jfo.openWriter();
        JavaWriter jw = new JavaWriter(writer);

        // Write package
        PackageElement pkg = elementUtils.getPackageOf(superClassName);
        if (!pkg.isUnnamed()) {
            jw.emitPackage(pkg.getQualifiedName().toString());
            jw.emitEmptyLine();
        } else {
            jw.emitPackage("");
        }

        jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
        jw.emitEmptyLine();
        jw.beginMethod(qualifiedClassName, "create",
                EnumSet.of(Modifier.PUBLIC), "String", "id");

        jw.beginControlFlow("if (id == null)");
        jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
        jw.endControlFlow();

        for (FactoryAnnotatedClass item : itemsMap.values()) {
            jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
            jw.emitStatement("return new %s()", item.getTypeElement()
                    .getQualifiedName().toString());
            jw.endControlFlow();
            jw.emitEmptyLine();
        }
        jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
        jw.endMethod();

        jw.endType();

        jw.close();
    }
}

13. 處理循環

注解處理器可能會有多次處理過程。官方文檔解釋如下:

Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing.

一個簡單的例子:第一輪處理調用了注解處理器的process()方法。對應到我們工廠模式的例子:FactoryProcessor被初始化一次(不是每次循環都會新建處理器對象),但process()可以被多次調用,如果新生成了 java 文件。這聽起來有點奇怪,是不?原因是,新生成的源代碼文件中也可能包含有@Factory注解,這些文件也將會被FactoryProcessor處理。
對于我們的PizzaStore來說,將會有三輪處理:

Round Input Output
1 CalzonePizza.java 
Tiramisu.java
MargheritaPizza.java
Meal.java
PizzaStore.java
MealFactory.java
2 MealFactory.java — none —
3 — none — — none —


我在這里解釋處理循環還有另外一個原因。如果你仔細看我們的FactoryProcessor代碼,你就會發現,我們把收集的數據保存到私有成員變量Map<String, FactoryGroupedClasses> factoryClasses。在第一輪循環中,我們檢測到MagheritaPizza, CalzonePizza 和 Tiramisu 然后我們生成了MealFactory.java 文件。在第二輪循環中我們把 MealFactory.java 當作輸入。由于 MealFactory.java 沒有@Factory注解,所以沒有數據被收集,我們預期不會有錯誤。但是我們得到了下面這個錯誤:
Attempt to recreate a file for type com.hannesdorfmann.annotationprocessing101.factory.MealFactory
這個問題在于,我們從來沒有清空過factoryClasses。那意味著,在第二輪處理中,process()仍然保存著第一輪收集的數據,然后想創建跟第一輪已經創建的相同文件(MealFactory), 這就導致了這個錯誤。在我們的例子中,我們知道,只有第一輪我們會檢測被@Factory注解的類,因此我們可以簡單的像下面這樣子修正:

public boolean process(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
    try {
        for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
            factoryClass.generateCode(elementUtils, filer);
        }

        // Clear to fix the problem
        factoryClasses.clear();

    } catch (IOException e) {
        error(null, e.getMessage());
    }
    ...
}

我知道還可以用其他方法來解決這個問題。比如,我們可以設置一個 boolean 標志等等。關鍵點在于:我們要記住,注解處理器會經過多輪循環處理(每一輪都是通過調用process()方法),我們不能覆蓋我們已經生成的代碼文件。

14. 分離處理器和注解

如果你有看我們github上的工廠處理器代碼。你就會發現,我們組織代碼到兩個模塊中。我們之所以那樣做,是因為我們想讓我們工廠例子的使用者在他們在工程中只編譯注解,包含處理器模塊只是為了編譯。這樣做的原因是,在發布程序時注解及生成的代碼會被打包到用戶程序中,而注解處理器則不會(注解處理器對用戶的程序運行是沒有用的)。假如注解處理器中使用到了其他第三方庫,那就會占用系統資源。如果你是一個 android 開發者,你可能會聽說過 65K 方法限制(一個android 的 .dex 文件,最多只可以有 65K 個方法)。如果你在注解處理器中使用了 Guava,并且把注解和處理器打包在一個包中,這樣的話,Android APK安裝包中不只是包含FactoryProcessor的代碼,而也包含了整個Guava的代碼。Guava有大約20000個方法。所以分開注解和處理器是非常有意義的。

15. 實例化生成的類

你已經看到了,在這個PizzaStore的例子中,生成了MealFactory類,它和其他手寫的 java 類沒有任何區別。你需要手動實例化它(跟其他 java 對象一樣):

public class PizzaStore {

  private MealFactory factory = new MealFactory();

  public Meal order(String mealName) {
    return factory.create(mealName);
  }

  ...
}

當然,你也可以使用反射的方法來實例化。這篇文章的主題是注解處理器,所以就不作過多的討論了。

16. 總結

我希望,你現在對注解處理器已經有比較深的印象了。我必須要再次強調的是:注解處理器是一個非常強大的工具,它可以幫助減少很多無聊代碼的編寫。我也想提醒的是,跟我的簡單工廠例子比起來,注解處理器可以做更多復雜的工作。例如,泛型的類型擦除,因為注解處理器是發生在類型擦除(type erasure)之前的。就像你所看到的,你在寫注解處理的時候,有兩個普遍的問題你必須要處理:1. 如果你想要在其他類中使用 ElementUtils, TypeUtils 和 Messager,你必須把它們作為參數傳給它們。2. 你必須做查詢Elements的操作。就像之前提到的,處理Element就和解析XML或者HTML一樣。對于HTML你可以是用jQuery,如果在注解處理器中,有類似于jQuery的庫那真是非常方便的。

最后一段是作者的提醒,原文如下:

Please note that parts of the code of FactoryProcessor has some edges and pitfalls. These “mistakes” are placed explicit by me to struggle through them as I explain common mistakes while writing annotation processors (like “Attempt to recreate a file”). If you start writing your own annotation processor based on FactoryProcessor DON’T copy and paste this pitfalls. Instead you should avoid them from the very beginning.

來自:http://qiushao.net/2015/07/07/Annotation-Processing-Tool%E8%AF%A6%E8%A7%A3/

 

 本文由用戶 ShellaKowal 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!