第10章 反射机制 - 编译时注解

优质
小牛编辑
130浏览
2023-12-01

在这篇文章中我将阐述如何实现一个注解处理器。首先我将向你解释什么是注解处理器,你可以使用这个强大的工具来做什么及不能做什么。接下来我们将一步一步来实现一个简单的注解处理器。

1. 一些基本概念

在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解。
注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。这里,我假设你已经了解什么是注解及如何自定义注解。如果你还未了解注解的话,可以查看官方文档。注解处理器在 Java 5 的时候就已经存在了,但直到 Java 6 (发布于2006看十二月)的时候才有可用的API。过了一段时间java的使用者们才意识到注解处理器的强大。所以最近几年它才开始流行。
一个特定注解的处理器以 java 源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java文件)作为输出。那意味着什么呢?你可以生成 java 代码!这些 java 代码在生成的.java文件中。因此你不能改变已经存在的java类,例如添加一个方法。这些生成的 java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。

2. AbstractProcessor

让我们来看一下处理器的 API。所有的处理器都继承了AbstractProcessor,如下所示:

  1. package com.example;
  2. import java.util.LinkedHashSet;
  3. import java.util.Set;
  4. import javax.annotation.processing.AbstractProcessor;
  5. import javax.annotation.processing.ProcessingEnvironment;
  6. import javax.annotation.processing.RoundEnvironment;
  7. import javax.annotation.processing.SupportedAnnotationTypes;
  8. import javax.annotation.processing.SupportedSourceVersion;
  9. import javax.lang.model.SourceVersion;
  10. import javax.lang.model.element.TypeElement;
  11. public class MyProcessor extends AbstractProcessor {
  12. @Override
  13. public boolean process(Set<? extends TypeElement> annoations,
  14. RoundEnvironment env) {
  15. return false;
  16. }
  17. @Override
  18. public Set<String> getSupportedAnnotationTypes() {
  19. Set<String> annotataions = new LinkedHashSet<String>();
  20. annotataions.add("com.example.MyAnnotation");
  21. return annotataions;
  22. }
  23. @Override
  24. public SourceVersion getSupportedSourceVersion() {
  25. return SourceVersion.latestSupported();
  26. }
  27. @Override
  28. public synchronized void init(ProcessingEnvironment processingEnv) {
  29. super.init(processingEnv);
  30. }
  31. }
  • init(ProcessingEnvironment processingEnv) :所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment 提供了一些实用的工具类Elements, TypesFiler。我们在后面将会使用到它们。

  • 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(),如下所示:

    1. @SupportedSourceVersion(value=SourceVersion.RELEASE_7)
    2. @SupportedAnnotationTypes({
    3. // Set of full qullified annotation type names
    4. "com.example.MyAnnotation",
    5. "com.example.AnotherAnnotation"
    6. })
    7. public class MyProcessor extends AbstractProcessor {
    8. @Override
    9. public boolean process(Set<? extends TypeElement> annoations,
    10. RoundEnvironment env) {
    11. return false;
    12. }
    13. @Override
    14. public synchronized void init(ProcessingEnvironment processingEnv) {
    15. super.init(processingEnv);
    16. }
    17. }

由于兼容性问题,特别是对于 android ,我建议重写getSupportedAnnotationTypes()getSupportedSourceVersion() ,而不是使用 @SupportedAnnotationTypes@SupportedSourceVersion

接下来你必须知道的事情是:注解处理器运行在它自己的 JVM 中。是的,你没看错。javac 启动了一个完整的 java 虚拟机来运行注解处理器。这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。使用 guava! 你可以使用依赖注入工具,比如dagger或者任何其他你想使用的类库。但不要忘记,即使只是一个小小的处理器,你也应该注意使用高效的算法及设计模式,就像你在开发其他 java 程序中所做的一样。

3. 注册你的处理器

你可能会问 “怎样注册我的注解处理器到 javac ?”。你必须提供一个.jar文件。就像其他 .jar 文件一样,你将你已经编译好的注解处理器打包到此文件中。并且,在你的 .jar 文件中,你必须打包一个特殊的文件javax.annotation.processing.ProcessorMETA-INF/services目录下。因此你的 .jar 文件目录结构看起来就你这样:

  1. MyProcess.jar
  2. -com
  3. -example
  4. -MyProcess.class
  5. -META-INF
  6. -services
  7. -javax.annotation.processing.Processor

javax.annotation.processing.Processor 文件的内容是一个列表,每一行是一个注解处理器的全称。例如:

  1. com.example.MyProcess
  2. com.example.AnotherProcess

4. 例子:工厂模式

现在可以举一个实际的例子了。我们使用maven 工具来作为我们的编译系统和依赖管理工具。我会把例子的代码放到 github上。
首先,我必须要说的是,想要找到一个可以使用注解处理器去解决的简单问题来当作教程,并不是一件容易的事。这篇教程中,我们将实现一个非常简单的工厂模式(不是抽象工厂模式)。它只是为了给你简明的介绍注解处理器的API而已。所以这个问题的程序,并不是那么有用,也不是一个真实开发中的例子。再次声明,你能学到的只是注解处理器的相关内容,而不是设计模式。

我们要解决的问题是:我们要实现一个 pizza 店,这个 pizza 店提供给顾客两种 pizza (Margherita 和 Calzone),还有甜点 Tiramisu(提拉米苏)。
简单看一下这段代码:
Meal.java

  1. package com.example.pizza;
  2. public interface Meal {
  3. public float getPrice();
  4. }

MargheritaPizza.java

  1. package com.example.pizza;
  2. public class MargheritaPizza implements Meal{
  3. @Override
  4. public float getPrice() {
  5. return 6.0f;
  6. }
  7. }

CalzonePizza.java

  1. package com.example.pizza;
  2. public class CalzonePizza implements Meal{
  3. @Override
  4. public float getPrice() {
  5. return 8.5f;
  6. }
  7. }

Tiramisu.java

  1. package com.example.pizza;
  2. public class Tiramisu implements Meal{
  3. @Override
  4. public float getPrice() {
  5. return 4.5f;
  6. }
  7. }

顾客要在我们的 pizza 店购买食物的话,就得输入食物的名称:
PizzaStore.java

  1. package com.example.pizza;
  2. import java.util.Scanner;
  3. public class PizzaStore {
  4. public Meal order(String mealName) {
  5. if (null == mealName) {
  6. throw new IllegalArgumentException("name of meal is null!");
  7. }
  8. if ("Margherita".equals(mealName)) {
  9. return new MargheritaPizza();
  10. }
  11. if ("Calzone".equals(mealName)) {
  12. return new CalzonePizza();
  13. }
  14. if ("Tiramisu".equals(mealName)) {
  15. return new Tiramisu();
  16. }
  17. throw new IllegalArgumentException("Unknown meal '" + mealName + "'");
  18. }
  19. private static String readConsole() {
  20. Scanner scanner = new Scanner(System.in);
  21. String meal = scanner.nextLine();
  22. scanner.close();
  23. return meal;
  24. }
  25. public static void main(String[] args) {
  26. System.out.println("welcome to pizza store");
  27. PizzaStore pizzaStore = new PizzaStore();
  28. Meal meal = pizzaStore.order(readConsole());
  29. System.out.println("Bill:$" + meal.getPrice());
  30. }
  31. }

正如你所见,在order()方法中,我们有许多 if 条件判断语句。并且,如果我们添加一种新的 pizza 的话,我们就得添加一个新的 if 条件判断。但是等一下,使用注解处理器和工厂模式,我们可以让一个注解处理器生成这些 if 语句。如此一来,我们想要的代码就像这样子:
PizzaStore.java

  1. package com.example.pizza;
  2. import java.util.Scanner;
  3. public class PizzaStore {
  4. private MealFactory factory = new MealFactory();
  5. public Meal order(String mealName) {
  6. return factory.create(mealName);
  7. }
  8. private static String readConsole() {
  9. Scanner scanner = new Scanner(System.in);
  10. String meal = scanner.nextLine();
  11. scanner.close();
  12. return meal;
  13. }
  14. public static void main(String[] args) {
  15. System.out.println("welcome to pizza store");
  16. PizzaStore pizzaStore = new PizzaStore();
  17. Meal meal = pizzaStore.order(readConsole());
  18. System.out.println("Bill:$" + meal.getPrice());
  19. }
  20. }

MealFactory 类应该是这样的:
MealFactory.java

  1. package com.example.pizza;
  2. public class MealFactory {
  3. public Meal create(String id) {
  4. if (id == null) {
  5. throw new IllegalArgumentException("id is null!");
  6. }
  7. if ("Calzone".equals(id)) {
  8. return new CalzonePizza();
  9. }
  10. if ("Tiramisu".equals(id)) {
  11. return new Tiramisu();
  12. }
  13. if ("Margherita".equals(id)) {
  14. return new MargheritaPizza();
  15. }
  16. throw new IllegalArgumentException("Unknown id = " + id);
  17. }
  18. }

5. @Factory Annotation

能猜到么,我们打算使用注解处理器生成MealFactory类。更一般的说,我们想要提供一个注解和一个处理器用来生成工厂类。
让我们看一下@Factory注解:
Factory.java

  1. package com.example.apt;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.TYPE)
  7. @Retention(RetentionPolicy.CLASS)
  8. public @interface Factory {
  9. /**
  10. * The name of the factory
  11. */
  12. Class<?> type();
  13. /**
  14. * The identifier for determining which item should be instantiated
  15. */
  16. String id();
  17. }

思想是这样的:我们注解那些食物类,使用type()表示这个类属于哪个工厂,使用id()表示这个类的具体类型。让我们将@Factory注解应用到这些类上吧:
MargheritaPizza.java

  1. package com.example.pizza;
  2. import com.example.apt.Factory;
  3. @Factory(type=MargheritaPizza.class, id="Margherita")
  4. public class MargheritaPizza implements Meal{
  5. @Override
  6. public float getPrice() {
  7. return 6.0f;
  8. }
  9. }

CalzonePizza.java

  1. package com.example.pizza;
  2. import com.example.apt.Factory;
  3. @Factory(type=CalzonePizza.class, id="Calzone")
  4. public class CalzonePizza implements Meal{
  5. @Override
  6. public float getPrice() {
  7. return 8.5f;
  8. }
  9. }

Tiramisu.java

  1. package com.example.pizza;
  2. import com.example.apt.Factory;
  3. @Factory(type=Tiramisu.class, id="Tiramisu")
  4. public class Tiramisu implements Meal{
  5. @Override
  6. public float getPrice() {
  7. return 4.5f;
  8. }
  9. }

你可能会问,我们是不是可以只将@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

  1. package com.example.apt;
  2. import java.util.LinkedHashMap;
  3. import java.util.LinkedHashSet;
  4. import java.util.Map;
  5. import java.util.Set;
  6. import javax.annotation.processing.AbstractProcessor;
  7. import javax.annotation.processing.Filer;
  8. import javax.annotation.processing.Messager;
  9. import javax.annotation.processing.ProcessingEnvironment;
  10. import javax.annotation.processing.RoundEnvironment;
  11. import javax.lang.model.SourceVersion;
  12. import javax.lang.model.element.TypeElement;
  13. import javax.lang.model.util.Elements;
  14. import javax.lang.model.util.Types;
  15. public class FactoryProcessor extends AbstractProcessor {
  16. private Types typeUtils;
  17. private Elements elementUtils;
  18. private Filer filer;
  19. private Messager messager;
  20. private Map<String, FactoryGroupedClasses> factoryClasses =
  21. new LinkedHashMap<String, FactoryGroupedClasses>();
  22. @Override
  23. public synchronized void init(ProcessingEnvironment processingEnv) {
  24. super.init(processingEnv);
  25. typeUtils = processingEnv.getTypeUtils();
  26. elementUtils = processingEnv.getElementUtils();
  27. filer = processingEnv.getFiler();
  28. messager = processingEnv.getMessager();
  29. }
  30. @Override
  31. public boolean process(Set<? extends TypeElement> arg0,
  32. RoundEnvironment arg1) {
  33. ...
  34. return false;
  35. }
  36. @Override
  37. public Set<String> getSupportedAnnotationTypes() {
  38. Set<String> annotataions = new LinkedHashSet<String>();
  39. annotataions.add(Factory.class.getCanonicalName());
  40. return annotataions;
  41. }
  42. @Override
  43. public SourceVersion getSupportedSourceVersion() {
  44. return SourceVersion.latestSupported();
  45. }
  46. }

getSupportedAnnotationTypes()方法中,我们指定@Factory注解将被这个处理器处理。

7. Elements and TypeMirrors

init()方法中,我们使用了以下类型:

  • Elements:一个用来处理Element的工具类(后面详细说明)
  • Types:一个用来处理TypeMirror的工具类(后面详细说明)
  • Filer:正如这个类的名字所示,你可以使用这个类来创建文件

在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是Element的一个特定类型。换句话说:Element代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构。在下面的例子中,我将添加注释来说明这个问题:

  1. package com.example;
  2. public class Foo { // TypeElement
  3. private int a; // VariableElement
  4. private Foo other; // VariableElement
  5. public Foo() {} // ExecuteableElement
  6. public void setA( // ExecuteableElement
  7. int newA // TypeElement
  8. ) {
  9. }
  10. }

你得换个角度来看源代码。它只是结构化的文本而已。它不是可以执行的。你可以把它当作 你试图去解析的XML 文件。或者一棵编译中创建的抽象语法树。就像在 XML 解析器中,有许多DOM元素。你可以通过一个元素找到它的父元素或者子元素。
例如:如果你有一个代表public class FooTypeElement,你就可以迭代访问它的子结点:

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

如你所见,Elements代表源代码,TypeElement代表源代码中的元素类型,例如类。然后,TypeElement并不包含类的相关信息。你可以从TypeElement获取类的名称,但你不能获取类的信息,比如说父类。这些信息可以通过TypeMirror获取。你可以通过调用element.asType()来获取一个ElementTypeMirror

(译注:关于getEnclosedElements, getEnclosingElement 的解释,参见官方文档

8. Searching For @Factory

让我们一步一步来实现process()方法吧。首先我们扫描所有被@Factory注解的类:

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

这里并没有什么高深的技术。roundEnv.getElementsAnnotatedWith(Factory.class) 返回一个被@Factory注解的元素列表。你可能注意到我避免说“返回一个被@Factory注解的类列表”。因为它的确是返回了一个Element列表。记住:Element可以是类,方法,变量等。所以,我们下一步需要做的是检查这个元素是否是一个类:

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

为什么需要这样做呢?因为我们要确保只有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的元素上,我们就抛出一个错误信息:

  1. @Override
  2. public boolean process(Set<? extends TypeElement> annotations,
  3. RoundEnvironment roundEnv) {
  4. for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
  5. if(annotatedElement.getKind() != ElementKind.CLASS) {
  6. error(annotatedElement, "Only classes can be annotated with @%s",
  7. Factory.class.getSimpleName());
  8. return true; // Exit processing
  9. }
  10. }
  11. return false;
  12. }
  13. private void error(Element e, String msg, Object... args) {
  14. messager.printMessage(
  15. Diagnostic.Kind.ERROR,
  16. String.format(msg, args),
  17. e);
  18. }

为了能够获取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

  1. package com.example.apt;
  2. import javax.lang.model.element.TypeElement;
  3. import javax.lang.model.type.DeclaredType;
  4. import javax.lang.model.type.MirroredTypeException;
  5. public class FactoryAnnotatedClass {
  6. private TypeElement annotatedClassElement;
  7. private String qualifiedSuperClassName;
  8. private String simpleTypeName;
  9. private String id;
  10. public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
  11. this.annotatedClassElement = classElement;
  12. Factory annotation = classElement.getAnnotation(Factory.class);
  13. id = annotation.id();
  14. if ("".equals(id)) {
  15. throw new IllegalArgumentException(
  16. String.format(
  17. "id() in @%s for class %s is null or empty! that's not allowed",
  18. Factory.class.getSimpleName(),
  19. classElement.getQualifiedName().toString()));
  20. }
  21. // Get the full QualifiedTypeName
  22. try {
  23. Class<?> clazz = annotation.type();
  24. qualifiedSuperClassName = clazz.getCanonicalName();
  25. simpleTypeName = clazz.getSimpleName();
  26. } catch (MirroredTypeException mte) {
  27. DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
  28. TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
  29. qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
  30. simpleTypeName = classTypeElement.getSimpleName().toString();
  31. }
  32. }
  33. /**
  34. * Get the id as specified in {@link Factory#id()}. return the id
  35. */
  36. public String getId() {
  37. return id;
  38. }
  39. /**
  40. * Get the full qualified name of the type specified in
  41. * {@link Factory#type()}.
  42. *
  43. * @return qualified name
  44. */
  45. public String getQualifiedFactoryGroupName() {
  46. return qualifiedSuperClassName;
  47. }
  48. /**
  49. * Get the simple name of the type specified in {@link Factory#type()}.
  50. *
  51. * @return qualified name
  52. */
  53. public String getSimpleFactoryGroupName() {
  54. return simpleTypeName;
  55. }
  56. /**
  57. * The original element that was annotated with @Factory
  58. */
  59. public TypeElement getTypeElement() {
  60. return annotatedClassElement;
  61. }
  62. }

看起来有很多代码,但是最重要的代码在构造函数中,你可以看到如下的代码:

  1. Factory annotation = classElement.getAnnotation(Factory.class);
  2. id = annotation.id();
  3. if ("".equals(id)) {
  4. throw new IllegalArgumentException(
  5. String.format(
  6. "id() in @%s for class %s is null or empty! that's not allowed",
  7. Factory.class.getSimpleName(),
  8. classElement.getQualifiedName().toString()));
  9. }

这里,我们获取@Factory注解,并检查id是否为空。如果id为空,我们将会抛出一个IllegalArgumentException异常。你可能感到疑惑的是,前面我们说了不要抛出异常,而是使用Messager。但这并不矛盾。我们在这里抛出一个内部异常,后面你将会看到我们在process()中捕获了这个异常。我们有两个这样做的理由:

  1. 我想说明你应该仍然像普通的Java程序一样编码。抛出和捕获异常被认为是一个好的Java编程实践。
  2. 如果我们想要在FactoryAnnotatedClass中正确地打印信息,我们也需要传入Messager对象,就像我们在错误处理一节中已经提到的,注解处理器必须成功结束,才能让Messager打印错误信息。如果我们想使用Messager打印一个错误信息,我们应该怎样通知process()发生了一个错误?最简单的方法,并且我认为了直观的方法,就是抛出一个异常然后让步process()捕获它。

接下来,我们将获取@Fractory注解中的type成员域。我们比较感兴趣的是合法的全名:

  1. // Get the full QualifiedTypeName
  2. try {
  3. Class<?> clazz = annotation.type();
  4. qualifiedSuperClassName = clazz.getCanonicalName();
  5. simpleTypeName = clazz.getSimpleName();
  6. } catch (MirroredTypeException mte) {
  7. DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
  8. TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
  9. qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
  10. simpleTypeName = classTypeElement.getSimpleName().toString();
  11. }

这有点棘手,因为这里的类型是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到一起。

  1. package com.example.apt;
  2. import java.io.IOException;
  3. import java.util.LinkedHashMap;
  4. import java.util.Map;
  5. import javax.annotation.processing.Filer;
  6. import javax.lang.model.util.Elements;
  7. public class FactoryGroupedClasses {
  8. private String qualifiedClassName;
  9. private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();
  10. public FactoryGroupedClasses(String qualifiedClassName) {
  11. this.qualifiedClassName = qualifiedClassName;
  12. }
  13. public void add(FactoryAnnotatedClass toInsert)
  14. throws IdAlreadyUsedException {
  15. FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
  16. if (existing != null) {
  17. throw new IdAlreadyUsedException(existing);
  18. }
  19. itemsMap.put(toInsert.getId(), toInsert);
  20. }
  21. public void generateCode(Elements elementUtils, Filer filer)
  22. throws IOException {
  23. ...
  24. }
  25. }

如你所见,它只是一个基本的Map<String, FactoryAnnotatedClass>,这个map用来映射@Factory.id()FactoryAnnotatedClass。我们选择使用Map,是因为我们想确保每一个id都的唯一的。使用map查找,这可以很容易实现。generateCode()将会被调用来生成工厂在的代码(稍后讨论)。

11. 匹配准则

让我们来继续实现process()方法。接下来我们要检查被注解的类至少有一个公有构造函数,不是抽象类,继承了特定的类,以及是一个public类:

  1. @Override
  2. public boolean process(Set<? extends TypeElement> annotations,
  3. RoundEnvironment roundEnv) {
  4. for (Element annotatedElement : roundEnv
  5. .getElementsAnnotatedWith(Factory.class)) {
  6. if (annotatedElement.getKind() != ElementKind.CLASS) {
  7. error(annotatedElement,
  8. "Only classes can be annotated with @%s",
  9. Factory.class.getSimpleName());
  10. return true; // Exit processing
  11. }
  12. // We can cast it, because we know that it of ElementKind.CLASS
  13. TypeElement typeElement = (TypeElement) annotatedElement;
  14. try {
  15. FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(
  16. typeElement); // throws IllegalArgumentException
  17. if (!isValidClass(annotatedClass)) {
  18. return true; // Error message printed, exit processing
  19. }
  20. } catch (IllegalArgumentException e) {
  21. // @Factory.id() is empty
  22. error(typeElement, e.getMessage());
  23. return true;
  24. }
  25. ...
  26. }
  27. return false;
  28. }
  29. private boolean isValidClass(FactoryAnnotatedClass item) {
  30. // Cast to TypeElement, has more type specific methods
  31. TypeElement classElement = item.getTypeElement();
  32. if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
  33. error(classElement, "The class %s is not public.", classElement
  34. .getQualifiedName().toString());
  35. return false;
  36. }
  37. // Check if it's an abstract class
  38. if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {
  39. error(classElement,
  40. "The class %s is abstract. You can't annotate abstract classes with @%",
  41. classElement.getQualifiedName().toString(),
  42. Factory.class.getSimpleName());
  43. return false;
  44. }
  45. // Check inheritance: Class must be childclass as specified in
  46. // @Factory.type();
  47. TypeElement superClassElement = elementUtils.getTypeElement(item
  48. .getQualifiedFactoryGroupName());
  49. if (superClassElement.getKind() == ElementKind.INTERFACE) {
  50. // Check interface implemented
  51. if (!classElement.getInterfaces().contains(
  52. superClassElement.asType())) {
  53. error(classElement,
  54. "The class %s annotated with @%s must implement the interface %s",
  55. classElement.getQualifiedName().toString(),
  56. Factory.class.getSimpleName(),
  57. item.getQualifiedFactoryGroupName());
  58. return false;
  59. }
  60. } else {
  61. // Check subclassing
  62. TypeElement currentClass = classElement;
  63. while (true) {
  64. TypeMirror superClassType = currentClass.getSuperclass();
  65. if (superClassType.getKind() == TypeKind.NONE) {
  66. // Basis class (java.lang.Object) reached, so exit
  67. error(classElement,
  68. "The class %s annotated with @%s must inherit from %s",
  69. classElement.getQualifiedName().toString(),
  70. Factory.class.getSimpleName(),
  71. item.getQualifiedFactoryGroupName());
  72. return false;
  73. }
  74. if (superClassType.toString().equals(
  75. item.getQualifiedFactoryGroupName())) {
  76. // Required super class found
  77. break;
  78. }
  79. // Moving up in inheritance tree
  80. currentClass = (TypeElement) typeUtils
  81. .asElement(superClassType);
  82. }
  83. }
  84. // Check if an empty public constructor is given
  85. for (Element enclosed : classElement.getEnclosedElements()) {
  86. if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
  87. ExecutableElement constructorElement = (ExecutableElement) enclosed;
  88. if (constructorElement.getParameters().size() == 0
  89. && constructorElement.getModifiers().contains(
  90. Modifier.PUBLIC)) {
  91. // Found an empty constructor
  92. return true;
  93. }
  94. }
  95. }
  96. // No empty constructor found
  97. error(classElement,
  98. "The class %s must provide an public empty default constructor",
  99. classElement.getQualifiedName().toString());
  100. return false;
  101. }

我们添加了一个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.PUBLICconstructorElement.getParameters().size() == 0

如果以上这些条件全都满足,则isValidClass()返回 true,否则,它打印一个错误信息然后返回 false

11. 组合被注解的类

一旦我们检查isValidClass()成功,我们就继续添加FactoryAnnotatedClass到相应的FactoryGroupedClasses中,如下所示:

  1. public boolean process(Set<? extends TypeElement> annotations,
  2. RoundEnvironment roundEnv) {
  3. for (Element annotatedElement : roundEnv
  4. .getElementsAnnotatedWith(Factory.class)) {
  5. ...
  6. try {
  7. FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(
  8. typeElement); // throws IllegalArgumentException
  9. if (!isValidClass(annotatedClass)) {
  10. return true; // Error message printed, exit processing
  11. }
  12. // Everything is fine, so try to add
  13. FactoryGroupedClasses factoryClass = factoryClasses
  14. .get(annotatedClass.getQualifiedFactoryGroupName());
  15. if (factoryClass == null) {
  16. String qualifiedGroupName = annotatedClass
  17. .getQualifiedFactoryGroupName();
  18. factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
  19. factoryClasses.put(qualifiedGroupName, factoryClass);
  20. }
  21. // Throws IdAlreadyUsedException if id is conflicting with
  22. // another @Factory annotated class with the same id
  23. factoryClass.add(annotatedClass);
  24. } catch (IllegalArgumentException e) {
  25. // @Factory.id() is empty --> printing error message
  26. error(typeElement, e.getMessage());
  27. return true;
  28. } catch (IdAlreadyUsedException e) {
  29. FactoryAnnotatedClass existing = e.getExisting();
  30. // Already existing
  31. error(annotatedElement,
  32. "Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id",
  33. typeElement.getQualifiedName().toString(),
  34. Factory.class.getSimpleName(), existing
  35. .getTypeElement().getQualifiedName().toString());
  36. return true;
  37. }
  38. ...

12. 代码生成

我们已经收集了所有被@Factory注解的类的信息,这些信息以FactoryAnnotatedClass的形式保存在FactoryGroupedClass中。现在我们可以为每一个工厂生成 java 文件了:

  1. public boolean process(Set<? extends TypeElement> annotations,
  2. RoundEnvironment roundEnv) {
  3. ...
  4. try {
  5. for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
  6. factoryClass.generateCode(elementUtils, filer);
  7. }
  8. } catch (IOException e) {
  9. error(null, e.getMessage());
  10. }
  11. return true;
  12. }

写 java 文件跟写其他文件完全一样。我们可以使用Filer提供的一个Writer对象来操作。我们可以用字符串拼接的方法写入我们生成的代码。幸运的是,Square公司(因为提供了许多非常优秀的开源项目二非常有名)给我们提供了JavaWriter,这是一个高级的生成Java代码的库:

  1. package com.example.apt;
  2. import java.io.IOException;
  3. import java.io.Writer;
  4. import java.util.EnumSet;
  5. import java.util.LinkedHashMap;
  6. import java.util.Map;
  7. import javax.annotation.processing.Filer;
  8. import javax.lang.model.element.Modifier;
  9. import javax.lang.model.element.PackageElement;
  10. import javax.lang.model.element.TypeElement;
  11. import javax.lang.model.util.Elements;
  12. import javax.tools.JavaFileObject;
  13. import com.squareup.javawriter.JavaWriter;
  14. public class FactoryGroupedClasses {
  15. /**
  16. * Will be added to the name of the generated factory class
  17. */
  18. private static final String SUFFIX = "Factory";
  19. private String qualifiedClassName;
  20. private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();
  21. public FactoryGroupedClasses(String qualifiedClassName) {
  22. this.qualifiedClassName = qualifiedClassName;
  23. }
  24. public void add(FactoryAnnotatedClass toInsert)
  25. throws IdAlreadyUsedException {
  26. FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
  27. if (existing != null) {
  28. throw new IdAlreadyUsedException(existing);
  29. }
  30. itemsMap.put(toInsert.getId(), toInsert);
  31. }
  32. public void generateCode(Elements elementUtils, Filer filer)
  33. throws IOException {
  34. TypeElement superClassName = elementUtils
  35. .getTypeElement(qualifiedClassName);
  36. String factoryClassName = superClassName.getSimpleName() + SUFFIX;
  37. JavaFileObject jfo = filer
  38. .createSourceFile(qualifiedClassName + SUFFIX);
  39. Writer writer = jfo.openWriter();
  40. JavaWriter jw = new JavaWriter(writer);
  41. // Write package
  42. PackageElement pkg = elementUtils.getPackageOf(superClassName);
  43. if (!pkg.isUnnamed()) {
  44. jw.emitPackage(pkg.getQualifiedName().toString());
  45. jw.emitEmptyLine();
  46. } else {
  47. jw.emitPackage("");
  48. }
  49. jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
  50. jw.emitEmptyLine();
  51. jw.beginMethod(qualifiedClassName, "create",
  52. EnumSet.of(Modifier.PUBLIC), "String", "id");
  53. jw.beginControlFlow("if (id == null)");
  54. jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
  55. jw.endControlFlow();
  56. for (FactoryAnnotatedClass item : itemsMap.values()) {
  57. jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
  58. jw.emitStatement("return new %s()", item.getTypeElement()
  59. .getQualifiedName().toString());
  60. jw.endControlFlow();
  61. jw.emitEmptyLine();
  62. }
  63. jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
  64. jw.endMethod();
  65. jw.endType();
  66. jw.close();
  67. }
  68. }

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来说,将会有三轮处理:

RoundInputOutput
1CalzonePizza.java Tiramisu.javaMargheritaPizza.javaMeal.javaPizzaStore.javaMealFactory.java
2MealFactory.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注解的类,因此我们可以简单的像下面这样子修正:

  1. public boolean process(Set<? extends TypeElement> annotations,
  2. RoundEnvironment roundEnv) {
  3. try {
  4. for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
  5. factoryClass.generateCode(elementUtils, filer);
  6. }
  7. // Clear to fix the problem
  8. factoryClasses.clear();
  9. } catch (IOException e) {
  10. error(null, e.getMessage());
  11. }
  12. ...
  13. }

我知道还可以用其他方法来解决这个问题。比如,我们可以设置一个 boolean 标志等等。关键点在于:我们要记住,注解处理器会经过多轮循环处理(每一轮都是通过调用process()方法),我们不能覆盖我们已经生成的代码文件。

14. 分离处理器和注解

如果你有看我们github上的工厂处理器代码。你就会发现,我们组织代码到两个模块中。我们之所以那样做,是因为我们想让我们工厂例子的使用者在他们在工程中只编译注解,包含处理器模块只是为了编译。这样做的原因是,在发布程序时注解及生成的代码会被打包到用户程序中,而注解处理器则不会(注解处理器对用户的程序运行是没有用的)。假如注解处理器中使用到了其他第三方库,那就会占用系统资源。如果你是一个 android 开发者,你可能会听说过 65K 方法限制(一个android 的 .dex 文件,最多只可以有 65K 个方法)。如果你在注解处理器中使用了 Guava,并且把注解和处理器打包在一个包中,这样的话,Android APK安装包中不只是包含FactoryProcessor的代码,而也包含了整个Guava的代码。Guava有大约20000个方法。所以分开注解和处理器是非常有意义的。

15. 实例化生成的类

你已经看到了,在这个PizzaStore的例子中,生成了MealFactory类,它和其他手写的 java 类没有任何区别。你需要手动实例化它(跟其他 java 对象一样):

  1. public class PizzaStore {
  2. private MealFactory factory = new MealFactory();
  3. public Meal order(String mealName) {
  4. return factory.create(mealName);
  5. }
  6. ...
  7. }

当然,你也可以使用反射的方法来实例化。这篇文章的主题是注解处理器,所以就不作过多的讨论了。

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.