Java註解處理器使用詳解

race604發表於2015-03-03

在這篇文章中,我將闡述怎樣寫一個註解處理器(Annotation Processor)。在這篇教程中,首先,我將向您解釋什麼是註解器,你可以利用這個強大的工具做什麼以及不能做什麼;然後,我將一步一步實現一個簡單的註解器。

一些基本概念

在開始之前,我們首先申明一個非常重要的問題:我們並不討論那些在執行時(Runtime)通過反射機制執行處理的註解,而是討論在編譯時(Compile time)處理的註解。

註解處理器是一個在javac中的,用來編譯時掃描和處理的註解的工具。你可以為特定的註解,註冊你自己的註解處理器。到這裡,我假設你已經知道什麼是註解,並且知道怎麼申明的一個註解型別。如果你不熟悉註解,你可以在這官方文件中得到更多資訊。註解處理器在Java 5開始就有了,但是從Java 6(2006年12月釋出)開始才有可用的API。過了一些時間,Java世界才意識到註解處理器的強大作用,所以它到最近幾年才流行起來。

一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這具體的含義什麼呢?你可以生成Java程式碼!這些生成的Java程式碼是在生成的.java檔案中,所以你不能修改已經存在的Java類,例如向已有的類中新增方法。這些生成的Java檔案,會同其他普通的手動編寫的Java原始碼一樣被javac編譯。

虛處理器AbstractProcessor

我們首先看一下處理器的API。每一個處理器都是繼承於AbstractProcessor,如下所示:

package com.example;

public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}
  • init(ProcessingEnvironment env): 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements, TypesFiler。後面我們將看到詳細的內容。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 這相當於每個處理器的主函式main()。你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。後面我們將看到詳細的內容。
  • getSupportedAnnotationTypes(): 這裡你必須指定,這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字串的集合,包含本處理器想要處理的註解型別的合法全稱。換句話說,你在這裡定義你的註解處理器註冊到哪些註解上。
  • getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported()。然而,如果你有足夠的理由只支援Java 6的話,你也可以返回SourceVersion.RELEASE_6。我推薦你使用前者。

在Java 7中,你也可以使用註解來代替getSupportedAnnotationTypes()getSupportedSourceVersion(),像這樣:

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法註解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

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

因為相容的原因,特別是針對Android平臺,我建議使用過載getSupportedAnnotationTypes()getSupportedSourceVersion()方法代替@SupportedAnnotationTypes@SupportedSourceVersion

接下來的你必須知道的事情是,註解處理器是執行它自己的虛擬機器JVM中。是的,你沒有看錯,javac啟動一個完整Java虛擬機器來執行註解處理器。這對你意味著什麼?你可以使用任何你在其他java應用中使用的的東西。使用guava。如果你願意,你可以使用依賴注入工具,例如dagger或者其他你想要的類庫。但是不要忘記,即使是一個很小的處理,你也要像其他Java應用一樣,注意演算法效率,以及設計模式

註冊你的處理器

你可能會問,我怎樣處理器MyProcessor到javac中。你必須提供一個.jar檔案。就像其他.jar檔案一樣,你打包你的註解處理器到此檔案中。並且,在你的jar中,你需要打包一個特定的檔案javax.annotation.processing.ProcessorMETA-INF/services路徑下。所以,你的.jar檔案看起來就像下面這樣:

打包進MyProcessor.jar中的javax.annotation.processing.Processor的內容是,註解處理器的合法的全名列表,每一個元素換行分割:

com.example.MyProcessor  
com.foo.OtherProcessor  
net.blabla.SpecialProcessor

MyProcessor.jar放到你的builpath中,javac會自動檢查和讀取javax.annotation.processing.Processor中的內容,並且註冊MyProcessor作為註解處理器。

例子:工廠模式

是時候來說一個實際的例子了。我們將使用maven工具來作為我們的編譯系統和依賴管理工具。如果你不熟悉maven,不用擔心,因為maven不是必須的。本例子的完成程式碼在Github上。

開始之前,我必須說,要為這個教程找到一個需要用註解處理器解決的簡單問題,實在並不容易。這裡我們將實現一個非常簡單的工廠模式(不是抽象工廠模式)。這將對註解處理器的API做一個非常簡明的介紹。所以,這個問題的程式並不是那麼有用,也不是一個真實世界的例子。所以在此申明,你將學習關於註解處理過程的相關內容,而不是設計模式。

我們將要解決的問題是:我們將實現一個披薩店,這個披薩店給消費者提供兩種披薩(“Margherita”和“Calzone”)以及提拉米蘇甜點(Tiramisu)。

看一下如下的程式碼,不需要做任何更多的解釋:

public interface Meal {  
  public float getPrice();
}

public class MargheritaPizza implements Meal {

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

public class CalzonePizza implements Meal {

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

public class Tiramisu implements Meal {

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

為了在我們的披薩店PizzsStore下訂單,消費者需要輸入餐(Meal)的名字。

public class PizzaStore {

  public Meal order(String mealName) {

    if (mealName == null) {
      throw new IllegalArgumentException("Name of the 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 + "'");
  }

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

正如你所見,在order()方法中,我們有很多的if語句,並且如果我們每新增一種新的披薩,我們都要新增一條新的if語句。但是等一下,使用註解處理和工廠模式,我們可以讓註解處理器來幫我們自動生成這些if語句。如此以來,我們期望的是如下的程式碼:

public class PizzaStore {

  private MealFactory factory = new MealFactory();

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

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

MealFactory應該是如下的樣子:

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);
  }
}

@Factory註解

你能猜到麼:我們想用註解處理器自動生成MealFactory。更一般的說,我們將想要提供一個註解和一個處理器來生成工廠類。

我們先來看一下@Factory註解:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS)
public @interface Factory {

  /**
   * 工廠的名字
   */
  Class type();

  /**
   * 用來表示生成哪個物件的唯一id
   */
  String id();
}

想法是這樣的:我們將使用同樣的type()註解那些屬於同一個工廠的類,並且用註解的id()做一個對映,例如從"Calzone"對映到"ClzonePizza"類。我們應用@Factory註解到我們的類中,如下:

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

  @Override public float getPrice() {
    return 6f;
  }
}
@Factory(
    id = "Calzone",
    type = Meal.class
)
public class CalzonePizza implements Meal {

  @Override public float getPrice() {
    return 8.5f;
  }
}
@Factory(
    id = "Tiramisu",
    type = Meal.class
)
public class Tiramisu implements Meal {

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

你可能會問你自己,我們是否可以只把@Factory註解應用到我們的Meal介面上?答案是,註解是不能繼承的。一個類class X被註解,並不意味著它的子類class Y extends X會自動被註解。在我們開始寫處理器的程式碼之前,我們先規定如下一些規則:

  1. 只有類可以被@Factory註解,因為介面或者抽象類並不能用new操作例項化;
  2. @Factory註解的類,必須至少提供一個公開的預設構造器(即沒有引數的建構函式)。否者我們沒法例項化一個物件。
  3. @Factory註解的類必須直接或者間接的繼承於type()指定的型別;
  4. 具有相同的type的註解類,將被聚合在一起生成一個工廠類。這個生成的類使用Factory字尾,例如type = Meal.class,將生成MealFactory工廠類;
  5. id只能是String型別,並且在同一個type組中必須唯一。

處理器

我將通過新增程式碼和一段解釋的方法,一步一步的引導你來構建我們的處理器。省略號(...)表示省略那些已經討論過的或者將在後面的步驟中討論的程式碼,目的是為了讓我們的程式碼有更好的可讀性。正如我們前面說的,我們完整的程式碼可以在Github上找到。好了,讓我們來看一下我們的處理器類FactoryProcessor的骨架:

@AutoService(Processor.class)
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 Set<String> getSupportedAnnotationTypes() {
    Set<String> annotataions = new LinkedHashSet<String>();
    annotataions.add(Factory.class.getCanonicalName());
    return annotataions;
  }

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

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

你看到在程式碼的第一行是@AutoService(Processor.class),這是什麼?這是一個其他註解處理器中引入的註解。AutoService註解處理器是Google開發的,用來生成META-INF/services/javax.annotation.processing.Processor檔案的。是的,你沒有看錯,我們可以在註解處理器中使用註解。非常方便,難道不是麼?在getSupportedAnnotationTypes()中,我們指定本處理器將處理@Factory註解。

Elements和TypeMirrors

init()中我們獲得如下引用:

  • Elements:一個用來處理Element的工具類(後面將做詳細說明);
  • Types:一個用來處理TypeMirror的工具類(後面將做詳細說明);
  • Filer:正如這個名字所示,使用Filer你可以建立檔案。

在註解處理過程中,我們掃描所有的Java原始檔。原始碼的每一個部分都是一個特定型別的Element。換句話說:Element代表程式的元素,例如包、類或者方法。每個Element代表一個靜態的、語言級別的構件。在下面的例子中,我們通過註釋來說明這個:

package com.example;    // PackageElement

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 Foo類的TypeElement元素,你可以遍歷它的孩子,如下:

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

正如你所見,Element代表的是原始碼。TypeElement代表的是原始碼中的型別元素,例如類。然而,TypeElement並不包含類本身的資訊。你可以從TypeElement中獲取類的名字,但是你獲取不到類的資訊,例如它的父類。這種資訊需要通過TypeMirror獲取。你可以通過呼叫elements.asType()獲取元素的TypeMirror

搜尋@Factory註解

我們來一步一步實現process()方法。首先,我們從搜尋被註解了@Factory的類開始:

@AutoService(Processor.class)
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 boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    // 遍歷所有被註解了@Factory的元素
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
          ...
    }
  }
 ...
}

這裡並沒有什麼高深的技術。roundEnv.getElementsAnnotatedWith(Factory.class))返回所有被註解了@Factory的元素的列表。你可能已經注意到,我們並沒有說“所有被註解了@Factory的類的列表”,因為它真的是返回Element的列表。請記住:Element可以是類、方法、變數等。所以,接下來,我們必須檢查這些Element是否是一個類:

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

    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

      // 檢查被註解為@Factory的元素是否是一個類
      if (annotatedElement.getKind() != ElementKind.CLASS) {
            ...
      }
   }
   ...
}

為什麼要這麼做?我們要確保只有class元素被我們的處理器處理。前面我們已經學習到類是用TypeElement表示。我們為什麼不這樣判斷呢if (! (annotatedElement instanceof TypeElement) )?這是錯誤的,因為介面(interface)型別也是TypeElement。所以在註解處理器中,我們要避免使用instanceof,而是配合TypeMirror使用EmentKind或者TypeKind

錯誤處理

init()中,我們也獲得了一個Messager物件的引用。Messager提供給註解處理器一個報告錯誤、警告以及提示資訊的途徑。它不是註解處理器開發者的日誌工具,而是用來寫一些資訊給使用此註解器的第三方開發者的。在官方文件中描述了訊息的不同級別。非常重要的是Kind.ERROR,因為這種型別的資訊用來表示我們的註解處理器處理失敗了。很有可能是第三方開發者錯誤的使用了@Factory註解(例如,給介面使用了@Factory註解)。這個概念和傳統的Java應用有點不一樣,在傳統Java應用中我們可能就丟擲一個異常Exception。如果你在process()中丟擲一個異常,那麼執行註解處理器的JVM將會崩潰(就像其他Java應用一樣),使用我們註解處理器FactoryProcessor第三方開發者將會從javac中得到非常難懂的出錯資訊,因為它包含FactoryProcessor的堆疊跟蹤(Stacktace)資訊。因此,註解處理器就有一個Messager類,它能夠列印非常優美的錯誤資訊。除此之外,你還可以連線到出錯的元素。在像IntelliJ這種現代的IDE(整合開發環境)中,第三方開發者可以直接點選錯誤資訊,IDE將會直接跳轉到第三方開發者專案的出錯的原始檔的相應的行。

我們重新回到process()方法的實現。如果遇到一個非類型別被註解@Factory,我們發出一個出錯資訊:

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

      // 檢查被註解為@Factory的元素是否是一個類
      if (annotatedElement.getKind() != ElementKind.CLASS) {
        error(annotatedElement, "Only classes can be annotated with @%s",
            Factory.class.getSimpleName());
        return true; // 退出處理
      }
      ...
    }

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

}

讓Messager顯示相關出錯資訊,更重要的是註解處理器程式必須完成執行而不崩潰,這就是為什麼在呼叫error()後直接return。如果我們不直接返回,process()將繼續執行,因為messager.printMessage( Diagnostic.Kind.ERROR)不會停止此程式。因此,如果我們在列印錯誤資訊以後不返回的話,我們很可能就會執行到一個NullPointerException等。就像我們前面說的,如果我們繼續執行process(),問題是如果在process()中丟擲一個未處理的異常,javac將會列印出內部的NullPointerException,而不是Messager中的錯誤資訊。

資料模型

在繼續檢查被註解@Fractory的類是否滿足我們上面說的5條規則之前,我們將介紹一個讓我們更方便繼續處理的資料結構。有時候,一個問題或者直譯器看起來如此簡單,以至於程式設計師傾向於用一個程式導向方式來寫整個處理器。但是你知道嗎?一個註解處理器任然是一個Java程式,所以我們需要使用物件導向程式設計、介面、設計模式,以及任何你將在其他普通Java程式中使用的技巧。

我們的FactoryProcessor非常簡單,但是我們仍然想要把一些資訊存為物件。在FactoryAnnotatedClass中,我們儲存被註解類的資料,比如合法的類的名字,以及@Factory註解本身的一些資訊。所以,我們儲存TypeElement和處理過的@Factory註解:

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 (StringUtils.isEmpty(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();
    }
  }

  /**
   * 獲取在{@link Factory#id()}中指定的id
   * return the id
   */
  public String getId() {
    return id;
  }

  /**
   * 獲取在{@link Factory#type()}指定的型別合法全名
   *
   * @return qualified name
   */
  public String getQualifiedFactoryGroupName() {
    return qualifiedSuperClassName;
  }

  /**
   * 獲取在{@link Factory#type()}{@link Factory#type()}指定的型別的簡單名字
   *
   * @return qualified name
   */
  public String getSimpleFactoryGroupName() {
    return simpleTypeName;
  }

  /**
   * 獲取被@Factory註解的原始元素
   */
  public TypeElement getTypeElement() {
    return annotatedClassElement;
  }
}

程式碼很多,但是最重要的部分是在建構函式中。其中你能找到如下的程式碼:

Factory annotation = classElement.getAnnotation(Factory.class);  
id = annotation.id(); // Read the id value (like "Calzone" or "Tiramisu")

if (StringUtils.isEmpty(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是否為空?如果為空,我們將丟擲IllegalArgumentException異常。你可能感到疑惑的是,前面我們說了不要丟擲異常,而是使用Messager。這裡仍然不矛盾。我們丟擲內部的異常,你在將在後面看到會在process()中捕獲這個異常。我這樣做出於一下兩個原因:

  1. 我想示意我們應該像普通的Java程式一樣編碼。丟擲和捕獲異常是非常好的Java程式設計實踐;
  2. 如果我們想要在FactoryAnnotatedClass中列印資訊,我需要也傳入Messager物件,並且我們在錯誤處理一節中已經提到,為了列印Messager資訊,我們必須成功停止處理器執行。如果我們使用Messager列印了錯誤資訊,我們怎樣告知process()出現了錯誤呢?最容易,並且我認為最直觀的方式就是丟擲一個異常,然後讓process()捕獲之。

接下來,我們將獲取@Fractory註解中的type成員。我們比較關心的是合法的全名:

try {  
      Class<?> clazz = annotation.type();
      qualifiedGroupClassName = clazz.getCanonicalName();
      simpleFactoryGroupName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {
      DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
      TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
      qualifiedGroupClassName = classTypeElement.getQualifiedName().toString();
      simpleFactoryGroupName = classTypeElement.getSimpleName().toString();
}

這裡有一點小麻煩,因為這裡的型別是一個java.lang.Class。這就意味著,他是一個真正的Class物件。因為註解處理是在編譯Java原始碼之前。我們需要考慮如下兩種情況:

  1. 這個類已經被編譯:這種情況是:如果第三方.jar包含已編譯的被@Factory註解.class檔案。在這種情況下,我們可以想try中那塊程式碼中所示直接獲取Class
  2. 這個還沒有被編譯:這種情況是我們嘗試編譯被@Fractory註解的原始碼。這種情況下,直接獲取Class會丟擲MirroredTypeException異常。幸運的是,MirroredTypeException包含一個TypeMirror,它表示我們未編譯類。因為我們已經知道它必定是一個類型別(我們已經在前面檢查過),我們可以直接強制轉換為DeclaredType,然後讀取TypeElement來獲取合法的名字。

好了,我們現在還需要一個資料結構FactoryGroupedClasses,它將簡單的組合所有的FactoryAnnotatedClasses到一起。

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>,這個對映表用來對映@Factory.id()到FactoryAnnotatedClass。我們選擇Map這個資料型別,是因為我們要確保每個id是唯一的,我們可以很容易通過map查詢實現。generateCode()方法將被用來生成工廠類程式碼(將在後面討論)。

匹配標準

我們繼續實現process()方法。接下來我們想要檢查被註解的類必須有隻要一個公開的建構函式,不是抽象類,繼承於特定的型別,以及是一個公開類:

public class FactoryProcessor extends AbstractProcessor {

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

    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

      ...

      // 因為我們已經知道它是ElementKind.CLASS型別,所以可以直接強制轉換
      TypeElement typeElement = (TypeElement) annotatedElement;

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

        if (!isValidClass(annotatedClass)) {
          return true; // 已經列印了錯誤資訊,退出處理過程
         }
       } catch (IllegalArgumentException e) {
        // @Factory.id()為空
        error(typeElement, e.getMessage());
        return true;
       }
          ...
   }

 private boolean isValidClass(FactoryAnnotatedClass item) {

    // 轉換為TypeElement, 含有更多特定的方法
    TypeElement classElement = item.getTypeElement();

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

    // 檢查是否是一個抽象類
    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;
    }

    // 檢查繼承關係: 必須是@Factory.type()指定的型別子類
    TypeElement superClassElement =
        elementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
    if (superClassElement.getKind() == ElementKind.INTERFACE) {
      // 檢查介面是否實現了                                       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 {
      // 檢查子類
      TypeElement currentClass = classElement;
      while (true) {
        TypeMirror superClassType = currentClass.getSuperclass();

        if (superClassType.getKind() == TypeKind.NONE) {
          // 到達了基本型別(java.lang.Object), 所以退出
          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())) {
          // 找到了要求的父類
          break;
        }

        // 在繼承樹上繼續向上搜尋
        currentClass = (TypeElement) typeUtils.asElement(superClassType);
      }
    }

    // 檢查是否提供了預設公開建構函式
    for (Element enclosed : classElement.getEnclosedElements()) {
      if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
        ExecutableElement constructorElement = (ExecutableElement) enclosed;
        if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers()
            .contains(Modifier.PUBLIC)) {
          // 找到了預設建構函式
          return true;
        }
      }
    }

    // 沒有找到預設建構函式
    error(classElement, "The class %s must provide an public empty default constructor",
        classElement.getQualifiedName().toString());
    return false;
  }
}

我們這裡新增了isValidClass()方法,來檢查是否我們所有的規則都被滿足了:

  • 必須是公開類:classElement.getModifiers().contains(Modifier.PUBLIC)
  • 必須是非抽象類:classElement.getModifiers().contains(Modifier.ABSTRACT)
  • 必須是@Factoy.type()指定的型別的子類或者介面的實現:首先我們使用elementUtils.getTypeElement(item.getQualifiedFactoryGroupName())建立一個傳入的Class(@Factoy.type())的元素。是的,你可以僅僅通過已知的合法類名來直接建立TypeElement(使用TypeMirror)。接下來我們檢查它是一個介面還是一個類:superClassElement.getKind() == ElementKind.INTERFACE。所以我們這裡有兩種情況:如果是介面,就判斷classElement.getInterfaces().contains(superClassElement.asType());如果是類,我們就必須使用currentClass.getSuperclass()掃描繼承層級。注意,整個檢查也可以使用typeUtils.isSubtype()來實現。
  • 類必須有一個公開的預設建構函式:我們遍歷所有的閉元素classElement.getEnclosedElements(),然後檢查ElementKind.CONSTRUCTORModifier.PUBLIC以及constructorElement.getParameters().size() == 0

如果所有這些條件都滿足,isValidClass()返回true,否者就列印錯誤資訊,並且返回false

組合被註解的類

一旦我們檢查isValidClass()成功,我們將新增FactoryAnnotatedClass到對應的FactoryGroupedClasses中,如下:

public class FactoryProcessor extends AbstractProcessor {

   private Map<String, FactoryGroupedClasses> factoryClasses =
      new LinkedHashMap<String, FactoryGroupedClasses>();

 @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      ...
      try {
        FactoryAnnotatedClass annotatedClass =
            new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException

          if (!isValidClass(annotatedClass)) {
          return true; // 錯誤資訊被列印,退出處理流程
        }

        // 所有檢查都沒有問題,所以可以新增了
        FactoryGroupedClasses factoryClass =
        factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
        if (factoryClass == null) {
          String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
          factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
          factoryClasses.put(qualifiedGroupName, factoryClass);
        }

        // 如果和其他的@Factory標註的類的id相同衝突,
        // 丟擲IdAlreadyUsedException異常
        factoryClass.add(annotatedClass);
      } catch (IllegalArgumentException e) {
        // @Factory.id()為空 --> 列印錯誤資訊
        error(typeElement, e.getMessage());
        return true;
      } catch (IdAlreadyUsedException e) {
        FactoryAnnotatedClass existing = e.getExisting();
        // 已經存在
        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;
      }
    }
    ...
}

程式碼生成

我們已經手機了所有的被@Factory註解的類儲存到為FactoryAnnotatedClass,並且組合到了FactoryGroupedClasses。現在我們將為每個工廠生成Java檔案了:

@Override
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物件,我們可以連線字串來寫我們生成的Java程式碼。幸運的是,Square公司(因為提供了許多非常優秀的開源專案二非常有名)給我們提供了JavaWriter,這是一個高階的生成Java程式碼的庫:

public class FactoryGroupedClasses {

  /**
   * 將被新增到生成的工廠類的名字中
   */
  private static final String SUFFIX = "Factory";

  private String qualifiedClassName;

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

  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);

    // 寫包名
    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();
  }
}

注意:因為JavaWriter非常非常的流行,所以很多處理器、庫、工具都依賴於JavaWriter。如果你使用依賴管理工具,例如maven或者gradle,假如一個庫依賴的JavaWriter的版本比其他的庫新,這將會導致一些問題。所以我建議你直接拷貝重新打包JavaWiter到你的註解處理器程式碼中(實際它只是一個Java檔案)。

更新:JavaWrite現在已經被JavaPoet取代了。

處理迴圈

註解處理過程可能會多於一次。官方javadoc定義處理過程如下:

註解處理過程是一個有序的迴圈過程。在每次迴圈中,一個處理器可能被要求去處理那些在上一次迴圈中產生的原始檔和類檔案中的註解。第一次迴圈的輸入是執行此工具的初始輸入。這些初始輸入,可以看成是虛擬的第0此的迴圈的輸出。

一個簡單的定義:一個處理迴圈是呼叫一個註解處理器的process()方法。對應到我們的工廠模式的例子中:FactoryProcessor被初始化一次(不是每次迴圈都會新建處理器物件),然而,如果生成了新的原始檔process()能夠被呼叫多次。聽起來有點奇怪不是麼?原因是這樣的,這些生成的檔案中也可能包含@Factory註解,它們還將會被FactoryProcessor處理。

例如我們的PizzaStore的例子中將會經過3次迴圈處理:

Round Input Output
1 CalzonePizza.javaTiramisu.javaMargheritaPizza.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作為輸入。因為在MealFactory中沒有檢測到@Factory註解,我們預期並沒有錯誤,然而我們得到如下的資訊:

Attempt to recreate a file for type com.hannesdorfmann.annotationprocessing101.factory.MealFactory

這個問題是因為我們沒有清除factoryClasses,這意味著,在第二輪的process()中,任然儲存著第一輪的資料,並且會嘗試生成在第一輪中已經生成的檔案,從而導致這個錯誤的出現。在我們的這個場景中,我們知道只有在第一輪中檢查@Factory註解的類,所以我們可以簡單的修復這個問題,如下:

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

      // 清除factoryClasses
      factoryClasses.clear();

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

我知道這有其他的方法來處理這個問題,例如我們也可以設定一個布林值標籤等。關鍵的點是:我們要記住註解處理過程是需要經過多輪處理的,並且你不能過載或者重新建立已經生成的原始碼。

分離處理器和註解

如果你已經看了我們的程式碼庫,你將發現我們組織我們的程式碼到兩個maven模組中了。我們這麼做是因為,我們想讓我們的工廠模式的例子的使用者,在他們的工程中只編譯註解,而包含處理器模組只是為了編譯。有點暈?我們舉個例子,如果我們只有一個包。如果另一個開發者想要把我們的工廠模式處理器用於他的專案中,他就必須包含@Factory註解和整個FactoryProcessor的程式碼(包括FactoryAnnotatedClass和FactoryGroupedClasses)到他們專案中。我非常確定的是,他並不需要在他已經編譯好的專案中包含處理器相關的程式碼。如果你是一個Android的開發者,你肯定聽說過65k個方法的限制(即在一個.dex檔案中,只能定址65000個方法)。如果你在FactoryProcessor中使用guava,並且把註解和處理器打包在一個包中,這樣的話,Android APK安裝包中不只是包含FactoryProcessor的程式碼,而也包含了整個guava的程式碼。Guava有大約20000個方法。所以分開註解和處理器是非常有意義的。

生成的類的例項化

你已經看到了,在這個PizzaStore的例子中,生成了MealFactory類,它和其他手寫的Java類沒有任何區別。進而,你需要就想其他Java物件,手動例項化它:

public class PizzaStore {

  private MealFactory factory = new MealFactory();

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

如果你是一個Android的開發者,你應該也非常熟悉一個叫做ButterKnife的註解處理器。在ButterKnife中,你使用@InjectView註解Android的View。ButterKnifeProcessor生成一個MyActivity$$ViewInjector,但是在ButterKnife你不需要手動呼叫new MyActivity$$ViewInjector()例項化一個ButterKnife注入的物件,而是使用Butterknife.inject(activity)。ButterKnife內部使用反射機制來例項化MyActivity$$ViewInjector()物件:

try {  
    Class<?> injector = Class.forName(clsName + "$$ViewInjector");
} catch (ClassNotFoundException e) { ... }

但是反射機制不是很慢麼,我們使用註解處理來生成原生程式碼,會不會導致很多的反射效能的問題?的確,反射機制的效能確實是一個問題。然而,它不需要手動去建立物件,確實提高了開發者的開發速度。ButterKnife中有一個雜湊表HashMap來快取例項化過的物件。所以MyActivity$$ViewInjector只是使用反射機制例項化一次,第二次需要MyActivity$$ViewInjector的時候,就直接衝雜湊表中獲得。

FragmentArgs非常類似於ButterKnife。它使用反射機制來建立物件,而不需要開發者手動來做這些。FragmentArgs在處理註解的時候生成一個特別的查詢表類,它其實就是一種雜湊表,所以整個FragmentArgs庫只是在第一次使用的時候,執行一次反射呼叫,一旦整個Class.forName()的Fragemnt的引數物件被建立,後面的都是原生程式碼執行了。

作為一個註解註解處理器的開發者,這些都由你來決定,為其他的註解器使用者,在反射和可用性上找到一個好的平衡。

總結

到此,我希望你對註解處理過程有一個非常深刻的理解。我必須再次說明一下:註解處理器是一個非常強大的工具,減少了很多無聊的程式碼的編寫。我也想提醒的是,註解處理器可以做到比我上面提到的工廠模式的例子複雜很多的事情。例如,泛型的型別擦除,因為註解處理器是發生在型別擦除(type erasure)之前的(譯者注:型別擦除可以參考這裡)。就像你所看到的,你在寫註解處理的時候,有兩個普遍的問題你需要處理:第一問題, 如果你想在其他類中使用ElementUtils, TypeUtils和Messager,你就必須把他們作為引數傳進去。在我為Android開發的註解器AnnotatedAdapter中,我嘗試使用Dagger(一個依賴注入庫)來解決這個問題。在這個簡單的處理中使用它聽起來有點過頭了,但是它確實很好用;第二個問題,你必須做查詢Elements的操作。就想我之前提到的,處理Element就解析XML或者HTML一樣。對於HTML你可以是用jQuery,如果在註解處理器中,有類似於jQuery的庫那那絕對是酷斃了。如果你知道有類似的庫,請在下面的評論告訴我。

請注意的是,在FactoryProcessor程式碼中有一些缺陷和陷阱。這些“錯誤”是我故意放進去的,是為了演示一些在開發過程中的常見錯誤(例如“Attempt to recreate a file”)。如果你想基於FactoryProcessor寫你自己註解處理器,請不要直接拷貝貼上這些陷阱過去,你應該從最開始就避免它們。

我在後續的部落格中將會寫註解處理器的單元測試,敬請關注。

相關文章