在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor)。在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么;然后,我将一步一步实现一个简单的注解器。

一些基本概念

在开始之前,我们首先申明一个非常重要的问题:我们并不讨论那些在运行时(Runtime)通过反射机制运行处理的注解,而是讨论在编译时(Compile time)处理的注解。

注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。到这里,我假设你已经知道什么是注解,并且知道怎么申明的一个注解类型。如果你不熟悉注解,你可以在这官方文档中得到更多信息。注解处理器在Java 5开始就有了,但是从Java 6(2006年12月发布)开始才有可用的API。过了一些时间,Java世界才意识到注解处理器的强大作用,所以它到最近几年才流行起来。

一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

虚处理器AbstractProcessor

我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:
package com.example;

public class MyProcessor extends AbstractProcessor {

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

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

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

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。后面我们将看到详细的内容。
process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。

在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
因为兼容的原因,特别是针对Android平台,我建议使用重载getSupportedAnnotationTypes()和getSupportedSourceVersion()方法代替@SupportedAnnotationTypes和@SupportedSourceVersion。

接下来的你必须知道的事情是,注解处理器是运行它自己的虚拟机JVM中。是的,你没有看错,javac启动一个完整Java虚拟机来运行注解处理器。这对你意味着什么?你可以使用任何你在其他java应用中使用的的东西。使用guava。如果你愿意,你可以使用依赖注入工具,例如dagger或者其他你想要的类库。但是不要忘记,即使是一个很小的处理,你也要像其他Java应用一样,注意算法效率,以及设计模式。

注册你的处理器

你可能会问,我怎样处理器MyProcessor到javac中。你必须提供一个.jar文件。就像其他.jar文件一样,你打包你的注解处理器到此文件中。并且,在你的jar中,你需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下。所以,你的.jar文件看起来就像下面这样:

 

打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是,注解处理器的合法的全名列表,每一个元素换行分割:
com.example.MyProcessor 
com.foo.OtherProcessor 
net.blabla.SpecialProcessor
把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器。

例子:工厂模式

是时候来说一个实际的例子了。我们将使用maven工具来作为我们的编译系统和依赖管理工具。如果你不熟悉maven,不用担心,因为maven不是必须的。本例子的完成代码在Github上。

开始之前,我必须说,要为这个教程找到一个需要用注解处理器解决的简单问题,实在并不容易。这里我们将实现一个非常简单的工厂模式(不是抽象工厂模式)。这将对注解处理器的API做一个非常简明的介绍。所以,这个问题的程序并不是那么有用,也不是一个真实世界的例子。所以在此申明,你将学习关于注解处理过程的相关内容,而不是设计模式。

我们将要解决的问题是:我们将实现一个披萨店,这个披萨店给消费者提供两种披萨(“Margherita”和“Calzone”)以及提拉米苏甜点(Tiramisu)。

看一下如下的代码,不需要做任何更多的解释:
public interface Meal { 
  public float getPrice();
}

public class MargheritaPizza implements Meal {

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

public class CalzonePizza implements Meal {

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

public class Tiramisu implements Meal {

  @Override public float getPrice() {
    return 4.5f;
  }
}
为了在我们的披萨店PizzsStore下订单,消费者需要输入餐(Meal)的名字。
public class PizzaStore {

  public Meal order(String mealName) {

    if (mealName == null) {
      throw new IllegalArgumentException("Name of the meal is null!");
    }

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

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

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

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

  public static void main(String[] args) throws IOException {
    PizzaStore pizzaStore = new PizzaStore();
    Meal meal = pizzaStore.order(readConsole());
    System.out.println("Bill: $" + meal.getPrice());
  }
}
正如你所见,在order()方法中,我们有很多的if语句,并且如果我们每添加一种新的披萨,我们都要添加一条新的if语句。但是等一下,使用注解处理和工厂模式,我们可以让注解处理器来帮我们自动生成这些if语句。如此以来,我们期望的是如下的代码:
public class PizzaStore {

  private MealFactory factory = new MealFactory();

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

  public static void main(String[] args) throws IOException {
    PizzaStore pizzaStore = new PizzaStore();
    Meal meal = pizzaStore.order(readConsole());
    System.out.println("Bill: $" + meal.getPrice());
  }
}
MealFactory应该是如下的样子:
public class MealFactory {

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

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

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

    throw new IllegalArgumentException("Unknown id = " + id);
  }
}
@Factory注解

你能猜到么:我们想用注解处理器自动生成MealFactory。更一般的说,我们将想要提供一个注解和一个处理器来生成工厂类。

我们先来看一下@Factory注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS)
public @interface Factory {

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

  /**
   * 用来表示生成哪个对象的唯一id
   */
  String id();
}
想法是这样的:我们将使用同样的type()注解那些属于同一个工厂的类,并且用注解的id()做一个映射,例如从"Calzone"映射到"ClzonePizza"类。我们应用@Factory注解到我们的类中,如下:
@Factory(
    id = "Margherita",
    type = Meal.class
)
public class MargheritaPizza implements Meal {

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

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

  @Override public float getPrice() {
    return 4.5f;
  }
}
你可能会问你自己,我们是否可以只把@Factory注解应用到我们的Meal接口上?答案是,注解是不能继承的。一个类class X被注解,并不意味着它的子类class Y extends X会自动被注解。在我们开始写处理器的代码之前,我们先规定如下一些规则:
只有类可以被@Factory注解,因为接口或者抽象类并不能用new操作实例化;
被@Factory注解的类,必须至少提供一个公开的默认构造器(即没有参数的构造函数)。否者我们没法实例化一个对象。
被@Factory注解的类必须直接或者间接的继承于type()指定的类型;
具有相同的type的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用Factory后缀,例如type = Meal.class,将生成MealFactory工厂类;
id只能是String类型,并且在同一个type组中必须唯一。

处理器

我将通过添加代码和一段解释的方法,一步一步的引导你来构建我们的处理器。省略号(...)表示省略那些已经讨论过的或者将在后面的步骤中讨论的代码,目的是为了让我们的代码有更好的可读性。正如我们前面说的,我们完整的代码可以在Github上找到。好了,让我们来看一下我们的处理器类FactoryProcessor的骨架:
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {

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

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

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

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

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    ...
  }
}
你看到在代码的第一行是@AutoService(Processor.class),这是什么?这是一个其他注解处理器中引入的注解。AutoService注解处理器是Google开发的,用来生成META-INF/services/javax.annotation.processing.Processor文件的。是的,你没有看错,我们可以在注解处理器中使用注解。非常方便,难道不是么?在getSupportedAnnotationTypes()中,我们指定本处理器将处理@Factory注解。

Elements和TypeMirrors

在init()中我们获得如下引用:
Elements:一个用来处理Element的工具类(后面将做详细说明);
Types:一个用来处理TypeMirror的工具类(后面将做详细说明);
Filer:正如这个名字所示,使用Filer你可以创建文件。

在注解处理过程中,我们扫描所有的Java源文件。源代码的每一个部分都是一个特定类型的Element。换句话说:Element代表程序的元素,例如包、类或者方法。每个Element代表一个静态的、语言级别的构件。在下面的例子中,我们通过注释来说明这个:
package com.example;    // PackageElement

public class Foo {        // TypeElement

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

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
你必须换个角度来看源代码,它只是结构化的文本,他不是可运行的。你可以想象它就像你将要去解析的XML文件一样(或者是编译器中抽象的语法树)。就像XML解释器一样,有一些类似DOM的元素。你可以从一个元素导航到它的父或者子元素上。

举例来说,假如你有一个代表public class Foo类的TypeElement元素,你可以遍历它的孩子,如下:
TypeElement fooClass = ... ; 
for (Element e : fooClass.getEnclosedElements()){ // iterate over children 
    Element parent = e.getEnclosingElement();  // parent == fooClass
}
正如你所见,Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。

搜索@Factory注解

我们来一步一步实现process()方法。首先,我们从搜索被注解了@Factory的类开始:
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {

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

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

    // 遍历所有被注解了@Factory的元素
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
          ...
    }
  }
 ...
}
这里并没有什么高深的技术。roundEnv.getElementsAnnotatedWith(Factory.class))返回所有被注解了@Factory的元素的列表。你可能已经注意到,我们并没有说“所有被注解了@Factory的类的列表”,因为它真的是返回Element的列表。请记住:Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个类:
@Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

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

      // 检查被注解为@Factory的元素是否是一个类
      if (annotatedE