注解处理101

原文链接http://hannesdorfmann.com/annotation-processing/annotationprocessing101

在这篇博客中,我想解释一下如何写注解处理器。因此,这使我的教程。首先,我打算解释什么是注解处理,你可以用这个强大工具做什么,最后是你不能做什么。在第二步中,我们将逐步实现一个简单地注解处理器。

为了在最开始澄清非常重要的事:我们不是在谈论通过在运行时(运行时 = 应用执行时)使用反射来评估注解。注解处理器发生在编译时(编译时 = java编译器编译java源码时)。

注解处理器是javac中的一个内嵌工具,用来在编译时搜索和处理注解。你可以对特定注解注册你自己的注解处理器。基于这点,我假定你已经知道什么是注解以及如何声明注解类型。如果你熟悉注解,你可以从java官方文档中找到更详细的信息。注解处理器从Java5就已经支持了,但是Java6(2006年12月发布)才提供了可用的API。它经过很长时间,直到Java世界实现了注解处理器的威力。所以它在最近几年才开始流行。

针对特定注解的注解处理器以java代码(或编译的二进制代码)作为输入,并生成文件(通常是.java文件)作为输出。真实含义是什么?你可以生成java代码。生成的java代码在生成的.java文件中。因此你不能修改现有java类,例如田建一个方法。生成的java文件可以使用javac编译,就像其他手工写的java源文件。

AbstractProcessor

让我们看看Processor 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提供一些有用的工具类ElementsTypes,和Filer。后面会用到它们。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env):这是每个处理器的main()方法。这里你可以写代码来搜索,评估和处理注解,并生成java文件。可以使用传递的RoundEnviroment参数查询带指定注解的元素,我们后面会看到。
  • getSupportedAnnotationTypes():这里指定注解处理器注册了那些注解。注意,返回值是字符串集合,这些字符串包含希望这个注解处理器处理的完全限定名称的注解类型。换句话说,这里定义了注解处理器注册的注解。
  • getSupportedSourceVersion():用于指定使用哪个Java版本。通常你返回SourceVersion.latestSupported()。然而,如果你有好的理由,你可以返回SourceVersion.RELEASE_6。我推荐使用SourceVersion.latestSupported()

使用Java7,你可以使用注解代替重写getSupportedAnnotationTypes()getSupportedSourceVersion,像这样:

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // Set of full qullified annotation type names
 })
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应用里做的那样。

注册你的处理器

你可以会问自己“我如何向javac注册我的处理器?”。你需要提供一个.jar文件。就像其他.jar文件一样,你将注解处理器打包(编译)成jar文件。而且你还需要在.jar文件的META-INF/services中添加指定名为javax.annotation.processing.Processor文件。所以你的.jar文件结构如下:

MyProcessor.jar
    - com
        - example
            - MyProcessor.class

    - META-INF
        - services
            - javax.annotation.processing.Processor

文件javax.annotation.processing.Processor(打包进MyProcessor.jar)的内容是对处理器完全限定类名的列表,以新行作为分隔符:

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

在buildpath中使用MyProcessor.jar,javac自动发现并读取javax.annotation.processing.Processor文件,并将MyProcessor注册为一个注解处理器。

例子:工厂模式

现在是时候来一个具体的例子了。我们将使用maven作为我们的构建系统和依赖管理工具。如果你不熟悉maven,不用担心,maven不是必须的。所有代码都可以在github上找到。

首先我得说,很难找一个简单的问题作为一个教程,该教程使用注解处理器来解决。这里我们打算实现一个非常简单的工程模式(不是抽象工厂模式)。它应该提供一个简单的注解处理API的介绍。所以,问题描述可能有点垃圾而且不存在于真实世界。再说一次,你将学会注解处理而不是设计模式。

问题是这样的:我们想实现一个披萨店。披萨店提供2种Pizzas(“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;
  }
}

为了在Pizzastore里下单,客户需要输入套餐名称:

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 {

  /**
   * The name of the factory
   */
  Class type();

  /**
   * The identifier for determining which item should be instantiated
   */
  String id();
}

我们的想法是,我们注解属于同type()工厂的类,以id()区分,这样我们将Calzone映射到CalzonePizza类。让我们将@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注解的类必须提供至少一个public的空默认构造函数(无参数的)。否则我们不能实例化新实例。
  3. @Factory注解的类必须直接或间接从指定type(或接口的实现)继承。
  4. 相同type@Factory注解分在一起,并生成一个工厂类。生成的类名有“Factory”后缀,例如type = Meal.class将生成MealFactory类。
  5. id限制为字符串,在他的类型组里必须是唯一的。

处理器

我在一个解释段落后引导你逐步添加代码行。3个点(...)表示忽略的代码,这些代码在之前段落讨论过或将在下一步添加。这样的目的是是的代码片段更具可读性。如前所述,完整的代码可以在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:顾名思义是文档管理器,你可以创建文件。

在注解处理时,我们搜索java源码文件。代码的每部分都是特定类型的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。你可以从元素导航到它的父或子元素。

例如,如果你有一个TypeElement代表public classFoo,你可以像这样搜索它的子元素:

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

你可以看到元素代表源码。TypeElement代表源码中的类型元素,如类。然而,TypeElement不包含累自己的信息。从TypeElement你将得到类名,但无法得到类的信息,如它的父类。这种信息通过TypeMirror访问。你可以调用element.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) {

    // Itearate over all @Factory annotated elements
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
        ...
    }
  }

 ...

}

没有火箭科学。roundEnv.getElementsAnnotatedWith(Factory.class))返回了带@Factory注解的元素列表。你可能注意到我避免说“返回带@Factory注解的类”,因为它实际返回的是Element列表。记住:Element可以是类,方法,变量等。因此,我们接着要检查元素是不是一个类:

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

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

      // Check if a class has been annotated with @Factory
      if (annotatedElement.getKind() != ElementKind.CLASS) {
            ...
      }
   }

   ...
}

这里发生了什么?我们想确保处理器只有处理类类型的元素。前面我们学习了雷士TypeElemts。那么为什么我们不使用if (! (annotatedElement instanceof TypeElement) )检查。这是一个错误的假定,因为接口也是TypeElement。所以在注解处理时,你应当避免使用instanceof,而是ElementKind或者带TypeMirror的TypeKind

错误处理

init()我们还检索了Messager的引用。一个Messager提供了注解处理器波高错误信息,警告和其他通告的方法。对你来说,它不是logger,注解处理器的开发者(甚至认为在开发处理器过程中可用)。Messager用来向第三方开发者写消息,他们在工程中使用你的注解处理器。在官方文档中描述了有不同级别的消息。非常重要的是[Kind.ERROR](http://docs.oracle.com/javase/7/docs/api/javax/tools/Diagnostic.Kind.html#ERROR),因为这种消息用来提示我们的注解处理器处理失败了。可能第三方开发者误用了我们的@Factory注解(例如,对接口使用@Factory)。这个概念与传统的java应用有一点不同,那些应用里你会抛出一个Exception。如果你在process()中抛出一个异常,接着执行注解处理的jvm发生崩溃(像其他java应用),使用我们FactoryProcessor的第三方开发者会从javac得到一个难以理解的异常,因为它包含FactoryProcess的崩溃站信息。因此注解处理去有这个Messager类。它打印一个漂亮的错误信息。而且,你可以连接到引起这个错误的元素。在现代IDE中,如Intellij,第三方开发者可以点击这个错误信息,IDE会调跳入第三方开发者工程中错误发生的源码和行。

回到process()方法实现。如果用户对非类对象使用了@Factory注解,我们会得到一个错误消息。

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

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

      // Check if a class has been annotated with @Factory
      if (annotatedElement.getKind() != ElementKind.CLASS) {
        error(annotatedElement, "Only classes can be annotated with @%s",
            Factory.class.getSimpleName());
        return true; // Exit processing
      }

      ...
    }

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的错误信息。

Datamodel

在继续检查注释了@Factory的类是否遵守了我们的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();
    }
  }

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

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

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

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

相当多的代码,但最重要的事情发生在构造函数中,这么我们找到下面的代码行:

Factory annotation = classElement.getAnnotation(Factory.class);
id = annotation.id(); // 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()捕获它。

接下来我们想得到@Factory注解的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。这就是说,它是一个真实的类对象。因为注解处理编译Java源代码执行之前,我们必须考虑两种情况:

  1. 类已经被编译:这种情况是第三方.jar文件包含已编译的带@Factory注解的.class文件。这种情况下,就像我们在try块做的,我们可以直接访问类。
  2. 类未被编译:这种情况是我们试图编译有@Factory注解的源码。试图直接访问类会抛出一个MirroredTypeException。幸运的是,MirroredTypeException包含一个TypeMirror代表我们为编译的类。因为我们知道它必须是class类型(前面已经检查过了),我们必须将它转型为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)) {

      ...

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

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

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

       ...
   }

 private boolean isValidClass(FactoryAnnotatedClass item) {

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

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

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

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

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

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

        // Moving up in inheritance tree
        currentClass = (TypeElement) typeUtils.asElement(superClassType);
      }
    }

    // Check if an empty public constructor is given
    for (Element enclosed : classElement.getEnclosedElements()) {
      if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
        ExecutableElement constructorElement = (ExecutableElement) enclosed;
        if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers()
            .contains(Modifier.PUBLIC)) {
          // Found an empty constructor
          return true;
        }
      }
    }

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

我们添加一个方法isValidClass(),它检查规则是否遵守:

  • 类必须是public:classElement.getModifiers().contains(Modifier.PUBLIC)
  • 类不能是abstract:classElement.getModifiers().contains(Modifier.ABSTRACT)
  • 类必须是子类或者@Factory.type()指定类的实现。首先使用elementUtils.getTypeElement(item.getQualifiedFactoryGroupName())创建传递Class(@Factory.type())的元素。是的,你做到了,你可以仅知道限定类名来创建TypeElement(带TypeMirror)。接着我们检查它是否是个接口或类:superClassElement.getKind() == ElementKind.INTERFACE。有两种情况:如果它是个接口,接着classElement.getInterfaces().contains(superClassElement.asType())。如果它是个类,接着调用currentClass.getSuperclass()来搜索继承层次。注意这个检查也可以使用typeUtils.isSubtype()来完成。
  • 类必须有一个公开的空的构造器:所有我们检索所有包含元素classElement.getEnclosedElements(),并检查ElementKind.CONSTRUCTOR,Modifier.PUBLICconstructorElement.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; // Error message printed, exit processing
        }

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

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

    ...
 }

代码生成

我们已经收集了所有带@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文件与写其他Java文件一样。我们使用Filer提供的Writer。我们可以把生成的代码写成一个字符串连接。幸运的是,因大量了不起的开源项目而知名的Square公司提供了JavaWriter,一个用来生成Java源码的高级库:

public class FactoryGroupedClasses {

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

  private String qualifiedClassName;

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

    ...

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

    TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
    String factoryClassName = superClassName.getSimpleName() + SUFFIX;

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

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

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

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

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

    jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
    jw.endMethod();

    jw.endType();

    jw.close();
  }
}

窍门:因为JavaWriter非常非常受欢迎,还有很多其他的处理器,库和工具依赖JavaWriter。如果你使用依赖管理工具,类似maven或gradle,可能引起问题,有可能一个库依赖较新版本的JavaWriter。因此我推荐直接拷贝并打包JavaWriter到你的注解处理器源码库(它只是个java文件)。

更新:看起来JavaWriter已经被JavaPoet代替。

处理回合

注解处理可能不止一圈。官方JavaWriterdoc定义处理如下:

注解处理分几轮串行进行。每一轮,处理器可能被要求处理源码和上轮生成的类文件找到的注解的子集。第一轮处理的输入是初始输入到工具的运行;这些初始输入可以被认为是虚拟第零轮处理的输出。

一个简单的定义:每轮处理都调用注解处理的process()。联想到我们的工厂例子:FactoryProcessor实例化一次(每轮创建新的处理器对象),但process()可被调用多次,如果有新的源文件创建了。听起来有点奇怪,不是么?原因是,生成的源码文件也可能包含@Factory注解的类,这些类要被FactoryProcessor处理。

例如我们的PizzaStore例子分3轮处理:

轮次 输入 输出
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);
      }

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

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

    ...
    return true;
}

我知道这里有其他的方法处理这个问题。例如我们还可以设一个布尔值等等。核心是:记住注解处理要多轮完成,你不能覆盖或重建已生成的源文件。

处理器和注解分离

如果你查看了我们工厂处理器的git仓库,你会看到我们将仓库组织成两个maven模块。我们这样做是因为希望工厂离子的用户两种可能,一种是在工程里只编译注解,另一种是只在编译时包含处理器模块。是不是晕了?例如,如果我们只有一种方式,其他开发者在自己的工程里就要既包含@Factory注解,又包含整个`FactoryProcessor(包括 FactoryAnnotatedClass和 FactoryGroupedClasses)源码。我相当肯定其他人不想在他的编译项目里有处理器类。如果你是一个android开发者,可能你听过65k方法数限制(一个android的.dex文件只能有65535个方法)。如果你在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](http://jakewharton.github.io/butterknife/)。在ButterKnife,你使用@InjectView注解android视图。ButterKnifeProcessor生成一个MyActivity$$ViewInjector类。但在ButterKnife中,你不想手工实例化ButterKnife来调用new MyActivity$$ViewInjector(),可以使用于ButterKnife.inject(activity)。ButterKnife内部使用反射来实例化MyActivity$$ViewInjector()

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

但是反射不慢么,而且我们试图引入大量的反射性能问题?是的,反射带来了性能问题。然而,它加上了开发,因为开发者不需要手动实例化对象。ButterKnife使用一个HashMap来“缓存”实例化对象。因此MyActivity$$ViewInjector只使用发射实例化一次。下次使用MyActivity$$ViewInjector将从HashMap中获取。

FragmentArgs工作原理类似于ButterKnife。它使用发射来实例化事物,否则开发者使用FragmentArgs不得不手工实例化。FragmentArgs生成一个特殊的“查找”类,而注解处理是一种HashMap。所以整个FragmentArgs库只在第一次实例化这个特殊的HashMap类时执行一次反射调用。一旦这个类由Class.forName()实例化之后,fragment参数注入就在本地Java代码中执行。

总而言之,这取决于你(注解处理器的开发者)在反射和注解处理器对其他用户的可用性之间找到一个良好的折中。

结论

我希望你现在理解了注解处理。我不得不再说一次:注解处理时非常强力的工具,帮助你减少编写模板代码。我还想提一下,使用注解处理器,你可以做远比例子更复杂的事情,例如泛型的类型擦除,因为注解处理在类型擦除前执行。你看到在写代码是这里有两个常见问题需要处理:第一,如果你想在其他类中使用ElementUtils,TypeUtils和Messager,那么你不得不以参数形式传递它们。在AnnotatedAdapter中,有一个注解处理器是针对android的,我试图使用Dagger(依赖注入)解决这个问题。感觉它有点大材小用了,不过它工作良好。第二个要解决的问题是要对元素进行“查询”。正如我之前说的,处理元素可以看做是解析XML或HTML。对于HTML你可以使用jQuery。同样的类似于将jQuery用于注解处理将是非常棒的。请在下面评论,如果你知道其他类似的库。

请注意FactoryProcessor的部分代码有一些边缘和陷阱。我明确的留下这些“错误”来解决它们,因为我解释了写注解处理器时常见错误(像“试图重建一个文件”)。如果你开始基于FactoryProcessor来写你自己的注解处理器,不要复制黏贴这个错误。相反你要在开始时就避免它们。

在将来的博客(注解处理102),我将写写注解处理器的单元测试。不过,我下一篇博文将是关于android的软件架构。敬请关注。

更新

我在droidcon Germany2015讨论了注解处理。我演讲的视频可以在youtube找到:

时间: 2024-08-28 09:49:03

注解处理101的相关文章

spring Aspect 实现自定义注解的日志记录

由于直接拦截所有的controller所以需要spring.xml中添加<aop:aspectj-autoproxy proxy-target-class="true" />  交由cglib代理. 使用只要在controller的method上加上@ActionControllerLog(channel="web",action="user_register",title="用户注册",isSaveRequest

hibernate注解标签及解释

3. * @author liuguangyi 4. * @content  ejb3注解的API定义在javax.persistence.*包里面. 5. * 6. * 注释说明: 7. * @Entity -- 将一个类声明为一个实体bean(即一个持久化POJO类) 8. * @Id -- 注解声明了该实体bean的标识属性(对应表中的主键). 9. * @Table -- 注解声明了该实体bean映射指定的表(table),目录(catalog)和schema的名字 10. * @Col

MyBatis学习笔记(四) 注解

使用MyBatis注解开发,可以省去类配置文件,简洁方便.但是比较复杂的SQL和动态SQL还是建议书写类配置文件. 注解还是不推荐使用的.只是了解了解!简单的CRUD可以使用注解.简单写写. 把之前的例子改成使用注解的. UserMapper.java 1 package com.cy.mybatis.mapper; 2 3 import java.util.List; 4 import java.util.Map; 5 6 import org.apache.ibatis.annotation

01 start.s汇编代码注解(RTEMS)

start.s 文件中汇编代码的注解(RTEMS) 作者:zhousm    2016年01月01日 处理器:S3C2440  ARM9 操作系统:RTEMS-4.10.2 源文件路径:rtems-4.10.2/c/src/lib/libbsp/arm/gp32/start/start.S 当处理器跳转到指定的地址开始执行时,即从该文件开始执行: 1 /* Some standard definitions...*/ 2 .equ PSR_MODE_USR, 0x10 3 .equ PSR_MO

Spring注解@Component、@Repository、@Service、@Controller区别

Spring注解@Component.@Repository.@Service.@Controller区别 Spring 2.5 中除了提供 @Component 注释外,还定义了几个拥有特殊语义的注释,它们分别是:@Repository.@Service 和 @Controller.在目前的 Spring 版本中,这 3 个注释和 @Component 是等效的,但是从注释类的命名上,很容易看出这 3 个注释分别和持久层.业务层和控制层(Web 层)相对应.虽然目前这 3 个注释和 @Comp

SpringMVC注解和Freemarker整合使用全步骤

SpringMVC现在是比较热门的一种框架了,使用起来感觉还是很不错的,现在我分享一下集体的配置和使用,希望对学习SpringMVC的朋友有用.一.首先我们做准备工作,下载Spring包,下载Freemarker包.二.配置web.xml. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

基于restful注解(spring4.0.2整合flex+blazeds+spring-mvc)&lt;一&gt;

摘自: http://www.blogjava.net/liuguly/archive/2014/03/10/410824.html 参考官网:1.http://livedocs.adobe.com/blazeds/1/blazeds_devguide/2.http://docs.spring.io/spring-flex/docs/1.5.2.RELEASE/reference/html/1)下载blazeds(turnkey4.0.x版本)网址:http://sourceforge.net/

8、Spring+Struts2+MyBaits(Spring注解+jdbc属性文件+log4j属性文件)

一.注解理论 使用注解来构造IoC容器 用注解来向Spring容器注册Bean.需要在applicationContext.xml中注册<context:component-scan base-package=”pagkage1[,pagkage2,…,pagkageN]”/>. 如:在base-package指明一个包 1 <context:component-scan base-package="cn.gacl.java"/> 表明cn.gacl.java包

spring与hibernate整合配置基于Annotation注解方式管理实务

1.配置数据源 数据库连接基本信息存放到properties文件中,因此先加载properties文件 1 <!-- jdbc连接信息 --> 2 <context:property-placeholder 3 location="classpath:io/shuqi/ssh/spring/transactionalAnnotation/jdbc.properties"/> 使用DBCP数据源配置xml如下 1 <!-- dbcp数据源配置 -->