Java注解處理器

jopen 9年前發布 | 70K 次閱讀 Java注解 Java開發

Java中的注解(Annotation)是一個很神奇的東西,特別現在有很多Android庫都是使用注解的方式來實現的。一直想詳細了解一下其中的原理。很有幸閱讀到一篇詳細解釋編寫注解處理器的文章。本文的原文是ANNOTATION PROCESSING 101,作者是Hannes Dorfmann。這是一篇好文,忍不住翻譯學習一下。以下是翻譯。


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

一些基本概念

在開始之前,我們首先申明一個非常重要的問題:我們并不討論那些在運行時(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() { }

}</pre>

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

    在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) { }
    

    }</pre>

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

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

    注冊你的處理器

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

    • MyProcessor.jar
      • com
      • </ul> </li>

          • example
          • </ul> </li> </ul> </li>

                • MyProcessor.class
                • </ul> </li> </ul> </li> </ul> </li>

                  • META-INF
                  • </ul> </li>

                      • services
                      • </ul> </li> </ul> </li>

                            • javax.annotation.processing.Processor
                            • </ul> </li> </ul> </li> </ul> </li> </ul>

                              打包進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; } }</pre>

                              為了在我們的披薩店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()); } }</pre>

                              正如你所見,在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()); } }</pre>

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

                              } }</pre> <h2>@Factory注解 </h2>

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

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

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

                              /**

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

                                /**

                              • 用來表示生成哪個對象的唯一id */ String id(); }</pre>

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

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

                                @Override public float getPrice() { return 6f; } }</pre>

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

                                @Override public float getPrice() { return 8.5f; } }</pre>

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

                                @Override public float getPrice() { return 4.5f; } }</pre>

                                你可能會問你自己,我們是否可以只把@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) { ... } }</pre>

                                你看到在代碼的第一行是@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
                                               ) {}
                              

                              }</pre>

                              你必須換個角度來看源代碼,它只是結構化的文本,他不是可運行的。你可以想象它就像你將要去解析的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)) {
                                    ...
                              }
                              

                              } ... }</pre>

                              這里并沒有什么高深的技術。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) {
                                      ...
                                }
                              

                              } ... }</pre>

                              為什么要這么做?我們要確保只有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); }

                              }</pre>

                              讓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; } }</pre>

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

                                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())); }</pre>

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

                              1. 我想示意我們應該像普通的Java程序一樣編碼。拋出和捕獲異常是非常好的Java編程實踐;
                              2. 如果我們想要在FactoryAnnotatedClass中打印信息,我需要也傳入Messager對象,并且我們在錯誤處理一節中已經提到,為了打印Messager信息,我們必須成功停止處理器運行。如果我們使用Messager打印了錯誤信息,我們怎樣告知process()出現了錯誤呢?最容易,并且我認為最直觀的方式就是拋出一個異常,然后讓process()捕獲之。
                              3. </ol>

                                接下來,我們將獲取@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來獲取合法的名字。
                                3. </ol>

                                  好了,我們現在還需要一個數據結構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 { ... } }</pre>

                                  正如你所見,這是一個基本的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;
                                  

                                  } }</pre>

                                  我們這里添加了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.CONSTRUCTOR、Modifier.PUBLIC以及constructorElement.getParameters().size() == 0。
                                  • </ul>

                                    如果所有這些條件都滿足,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;
                                      }
                                    }
                                    ...
                                    

                                    }</pre>

                                    代碼生成

                                    我們已經手機了所有的被@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;
                                    

                                    }</pre>

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

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

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

                                      處理循環

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

                                      注解處理過程是一個有序的循環過程。在每次循環中,一個處理器可能被要求去處理那些在上一次循環中產生的源文件和類文件中的注解。第一次循環的輸入是運行此工具的初始輸入。這些初始輸入,可以看成是虛擬的第0此的循環的輸出。

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

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

                                      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作為輸入。因為在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; }</pre>

                                      我知道這有其他的方法來處理這個問題,例如我們也可以設置一個布爾值標簽等。關鍵的點是:我們要記住注解處理過程是需要經過多輪處理的,并且你不能重載或者重新創建已經生成的源代碼。

                                      分離處理器和注解

                                      如果你已經看了我們的代碼庫,你將發現我們組織我們的代碼到兩個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); } ... }</pre>

                                      如果你是一個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寫你自己注解處理器,請不要直接拷貝粘貼這些陷阱過去,你應該從最開始就避免它們。

                                      我在后續的博客中將會寫注解處理器的單元測試,敬請關注。


                                      來自:http://www.race604.com/annotation-processing/

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