Java Annotation Processor

Keywords: Java ButterKnife Android Maven

Some basic concepts

Before we start, let's first state a very important issue: we don't discuss annotations that are processed at Runtime through a reflection mechanism, but rather annotations that are processed at Compile time.

Annotation Processor is a tool of javac that scans and processes annotations at compile time. You can customize annotations and register the corresponding annotation processor. Here, I assume that you already know what annotations are and how to declare an annotation. If you are not familiar with annotations, you can be here. Official documents For more information. Annotation processors have been available since Java 5, but only API s have been available since Java 6 (released in December 2006). It took some time for the Java world to realize the power of annotation processors, so it was only in recent years that annotation processors became popular.

An annotation processor that takes java code (or compiled bytecode) as input and generates files (usually. java files) as output. What exactly does this mean? You can generate java code! The generated java code is in the generated. java file, so you can't modify existing java classes, such as adding methods to existing classes. These generated java files are compiled by javac like other commonly written java source code manually.

Virtual Processor AbstractProcessor

Let's first look at the processor API. Each processor inherits from AbstractProcessor, as follows:

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 (Processing Environment env): Each annotated processor class must have an empty constructor. However, there is a special init() method that will be called by the annotation processing tool and input the Processing Enviroment parameter. Processing Enviroment provides many useful tool classes Elements, Types and Filer s. We'll see the details later.
  • Process (Set <? Extends TypeElement > annotations, Round Environment env): This is equivalent to the main function () of each processor. Here you write your code for scanning, evaluating, and processing annotations, as well as generating Java files. Annotations represent the TypeElement input parameter RoundEnviroment for all Annotations processed, allowing you to query annotated elements that contain specific annotations. We'll see the details later. The return value indicates whether the annotation is intercepted (without further processing).
  • GetSupported Annotation Types (): Here you have to specify which annotation processor is registered for. Note that its return value is a collection of strings containing the legal full name of the annotation type that the processor wants to process. In other words, here you define which annotations your annotation processor registers on.
  • getSupportedSourceVersion(): Used to specify the Java version you use. Usually SourceVersion.latestSupported() is returned here. However, if you have enough reasons to support Java 6 only, you can also return to SourceVersion.RELEASE_6. I recommend the former.

In Java 7, you can also use annotations instead of getSupported Annotation Types () and getSupported Source Version (), like this:

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // Collection of full names for legal annotations
 })
public class MyProcessor extends AbstractProcessor {

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

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

The next thing you have to know is that the annotation processor runs its own virtual machine JVM. Yes, you're right. javac starts a full Java virtual machine to run the annotation processor. What does this mean to you? You can use anything you use in other Java applications. Use guava. If you want, you can use dependency injection tools, such as dagger or other libraries you want. But don't forget, even if it's a small process, you have to pay attention to algorithmic efficiency and design patterns, just like other Java applications.

Register your processor

You might ask how I registered my processor MyProcessor with javac. You must provide A. jar file. Like other. jar files, you pack your annotation processor into this file. Also, in your jar, you need to package a specific file, javax.annotation.processing.Processor, into the META-INF/services path. So your.Jar file looks like the following:

  • MyProcessor.jar
    • com
      • example
        • MyProcessor.class
    • META-INF
      • services
        • javax.annotation.processing.Processor

The content of javax.annotation.processing.Processor packaged in MyProcessor.jar is a list of legitimate full names of annotated processors, with each element divided in line breaks:

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

Put MyProcessor.jar in your builpath, and javac automatically checks and reads the contents of javax.annotation.processing.Processor, and registers MyProcessor as an annotation processor.

Example: Factory model

It's time to give a practical example. We will use the Maven tool as our compilation system and dependency management tool. If you are not familiar with maven, don't worry, because Maven is not necessary. The complete code for this example is Github Up.

Before I start, I must say that it's not easy to find a simple problem for this tutorial that needs to be solved with annotation processors. Here we will implement a very simple factory pattern (not an abstract factory pattern). This will give a very concise introduction to the API of the annotation processor. So the program for this problem is not that useful, nor is it a real world example. So it's stated here that you'll learn about annotation processing, not design patterns.

The problem we're going to solve is that we're going to implement a pizza shop that provides consumers with two kinds of pizza ("Margherita" and "Calzone") and Tiramisu dessert.

Look at the following code without any further explanation:

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

In order to place an order at our pizza store, PizzsStore, consumers need to enter the name of the 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());
  }
}

As you can see, in the order() method, we have a lot of if statements, and if we add a new pizza, we add a new if statement. But wait a minute, using annotation processing and factory mode, we can let the annotation processor help us automatically generate these if statements. So what we expect is the following code:

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 should look like this:

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 annotation

Can you guess: We want to use annotation processors to automatically generate MealFactory. More generally, we would like to provide an annotation and a processor to generate factory classes.

Let's take a look first. @Factory Notes:

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

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

  /**
   * The unique id used to represent which object to generate
   */
  String id();
}

The idea is this: We will annotate classes belonging to the same factory with the same type(), and make a mapping with the id() of the annotation, such as from Calzone to Clzone Pizza. We apply @Factory Annotated to our class, as follows:

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

You may ask yourself, can we just do this? @Factory Annotations applied to our Meal interface? The answer is that annotations cannot be inherited. The annotation of a class X does not mean that its subclass class class Y extends X will be annotated automatically. Before we start writing processor code, let's specify the following rules:

  1. Only classes can be @Factory Annotations, because interfaces or abstract classes cannot be instantiated with new operations;
  2. Classes annotated by @Factory must at least provide an open default constructor (that is, a constructor without parameters). No, we can't instantiate an object.
  3. Classes annotated by @Factory must inherit directly or indirectly from the type specified by type().
  4. Annotation classes with the same type will be aggregated to produce a factory class. This generated class uses the Factory suffix, such as type = Meal.class, which generates the MealFactory class.
  5. id can only be String type and must be unique in the same type group.

processor

I'll guide you step by step to build our processor by adding code and explaining a paragraph. The ellipsis sign (...) denotes the omission of code that has been discussed or will be discussed in the next steps in order to make our code more readable. As we said earlier, our complete code can be Github Find it. Well, let's take a look at the skeleton of our processor class 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) {
    ...
  }
}

You see in the first line of the code, @AutoService(Processor.class), what is this? This is an annotation introduced in other annotation processors. The AutoService Annotation Processor was developed by Google to generate META-INF/services/javax.annotation.processing.Processor files. Yes, you're right. We can use annotations in annotation processors. It's very convenient, isn't it? In getSupported Annotation Types (), we specify that the processor will process the @Factory annotation.

Elements and TypeMirrors

In init(), we get the following reference:

  • Elements: A tool class for dealing with Elements (detailed later);
  • Types: A tool class for handling TypeMirror (described in detail later);
  • Filer: As the name suggests, you can create files with Filer.

During annotation processing, we scan all Java source files. Each part of the source code is a particular type of Element. In other words: Elements represent elements of a program, such as packages, classes, or methods. Each element represents a static, language-level component. In the following example, we illustrate this by commenting:

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

You have to look at the source code in a different way. It's just structured text. It's not operational. You can imagine that it's like the XML file you're going to parse (or the abstract syntax tree in the compiler). Like the XML interpreter, there are some DOM-like elements. You can navigate from one element to its parent or child elements.

For example, if you have a TypeElement element representing the public class Foo class, you can traverse its children as follows:

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

As you can see, Element represents source code. TypeElement represents type elements in source code, such as classes. However, TypeElement does not contain information about the class itself. You can get the name of the class from TypeElement, but you can't get information about the class, such as its parent class. This information needs to be obtained through TypeMirror. You can get the TypeMirror of the element by calling elements.asType().

Search for @Factory annotations

Let's implement the process() method step by step. First, we start by searching for classes annotated with @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) {

    // Traverse through all elements annotated with @Factory
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
          ...
    }
  }
 ...
}

There is no advanced technology here. RoundEnv. getElements Annotated With (Factory. class) returns a list of all elements annotated with @Factory. As you may have noticed, we did not say "a list of all classes annotated with @Factory" because it really returns a list of Elements. Remember: Elements can be classes, methods, variables, etc. So, next, we have to check whether these Elements are a class:

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

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

      // Check if the element annotated @Factory is a class?
      if (annotatedElement.getKind() != ElementKind.CLASS) {
            ...
      }
   }
   ...
}

Why did you do that? We want to make sure that only class elements are processed by our processor. We've learned earlier that classes are represented by TypeElement. Why don't we judge this way if (!)? This is wrong because the interface type is also a TypeElement. So in annotation processors, instead of using instanceof, we use EmentKind or TypeKind with TypeMirror.

error handling

In init(), we also get a reference to a Messager object. Messager provides annotation processors with a way to report errors, warnings, and prompts. It is not a logging tool for annotation processor developers, but is used to write some information to third-party developers who use this annotator. stay Official documents It describes the different levels of messages. Very important is Kind.ERROR Because this type of information is used to indicate that our annotation processor failed to process. It's likely that third-party developers misused @Factory annotations (for example, for interfaces). This concept is somewhat different from traditional Java applications, where we might throw an exception Exception. If you throw an exception in process(), the JVM running the annotation processor will crash (just like other Java applications). Third-party developers using our annotation processor FactoryProcessor will get very difficult error information from javac because it contains Stacktace information of FactoryProcessor. Therefore, the annotation processor has a Messager class that can print very beautiful error messages. In addition, you can connect to the wrong element. In a modern IDE (Integrated Development Environment) like IntelliJ, third-party developers can click on error messages directly, and IDE will jump directly to the corresponding line of the source file of a third-party developer project's error.

Let's go back to the implementation of the process() method. If we encounter a non-class type annotated @Factory, we send an error message:

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

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

      // Check if the element annotated @Factory is a class?
      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);
  }

}

Let Messager display relevant error information, and more importantly, annotate that the processor program must run without crashing, which is why it returns directly after calling error(). If we do not return directly, the process() will continue to run because messager. printMessage (Diagnostic. Kind. ERROR) will not stop the process. Therefore, if we don't return the wrong information after printing it, we will probably run to a NullPointerException and so on. As we said earlier, if we continue to run process(), the problem is that if an unhandled exception is thrown in process(), javac will print out the internal NullPointerException instead of the error message in Messager.

data model

Before continuing to check whether the annotated @Fractory class meets the five rules we mentioned above, we'll introduce a data structure that makes it easier for us to continue processing. Sometimes a problem or interpreter seems so simple that programmers tend to write the entire processor in a process-oriented way. But you know what? An annotation processor is still a Java program, so we need to use object-oriented programming, interfaces, design patterns, and any techniques you will use in other ordinary Java programs.

Our FactoryProcessor is very simple, but we still want to store some information as objects. In FactoryAnnotated Class, we store data about annotated classes, such as the name of the legitimate class and some information about the @Factory annotation itself. So, we save the TypeElement and processed @Factory annotations:

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

  /**
   * Gets the ID specified in {@link Factory#id()}
   * return the id
   */
  public String getId() {
    return id;
  }

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


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

  /**
   * Get the original element annotated by @Factory
   */
  public TypeElement getTypeElement() {
    return annotatedClassElement;
  }
}

There's a lot of code, but the most important part is in the constructor. Among them, you can find the following code:

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

Here we get the @Factory annotation and check if the id is empty. If null, we will throw an IllegalArgumentException exception. You may be wondering, as we said earlier, don't throw exceptions, but use Messager. There is still no contradiction here. We throw an internal exception, which you will see later in the process(). I do this for two reasons:

  1. I want to signal that we should code like normal Java programs. Throwing and catching exceptions is a good Java programming practice.
  2. If we want to print messages in FactoryAnnotated Class, I need to also pass in the Messager object, and as we mentioned in the Error Handling section, in order to print messages, we must successfully stop the processor running. If we print an error message using Messager, how do we tell process() that there is an error? The easiest, and I think the most intuitive way is to throw an exception and let process() catch it.

Next, we'll get the type member in the @Fractory annotation. We are more concerned about legal full names:

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

There's a little trouble here, because the type here is a java.lang.Class. This means that he is a real Class object. Because annotation processing is before compiling Java source code. We need to consider the following two situations:

  1. This class has been compiled: in this case, if the third party. jar contains the compiled @Factory annotated. class file. In this case, we can think of getting Class directly as shown in the code in try.
  2. This hasn't been compiled yet: In this case, we're trying to compile the source code annotated by @Fractory. In this case, getting Class directly throws a MirroredTypeException exception. Fortunately, MirroredTypeException contains a TypeMirror that represents our uncompiled class. Because we already know that it must be a class type (which we checked earlier), we can directly force the conversion to Declared Type, and then read TypeElement to get the legal name.

Well, now we need a data structure, FactoryGroupedClasses, which simply combines all FactoryAnnotated Classes together.

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 {
    ...
  }
}

As you can see, this is a basic Map < String, FactoryAnnotatedClass >, which maps @Factory.id() to FactoryAnnotatedClass. We chose Map as the data type because we wanted to make sure that each ID was unique and that we could easily find it through map. The generateCode() method is used to generate factory class code (discussed later).

Matching standard

We continue to implement the process() method. Next we want to check that the annotated class must have only one public constructor, not an abstract class, inherit from a particular type, and be a public class:

public class FactoryProcessor extends AbstractProcessor {

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

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

      ...

      // Because we already know it's ElementKind.CLASS type, we can cast it directly.
      TypeElement typeElement = (TypeElement) annotatedElement;

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

        if (!isValidClass(annotatedClass)) {
          return true; // The error message has been printed and the process has been dropped out.
         }
       } catch (IllegalArgumentException e) {
        // @ Factory.id() is empty
        error(typeElement, e.getMessage());
        return true;
       }
          ...
   }

 private boolean isValidClass(FactoryAnnotatedClass item) {

    // Convert to TypeElement with more 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 is 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 relationships: Must be a type subclass specified by @Factory.type()
    TypeElement superClassElement =
        elementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
    if (superClassElement.getKind() == ElementKind.INTERFACE) {
      // Check whether the interface implements 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 subclasses
      TypeElement currentClass = classElement;
      while (true) {
        TypeMirror superClassType = currentClass.getSuperclass();

        if (superClassType.getKind() == TypeKind.NONE) {
          // Has reached the basic type (java.lang.Object), so exits
          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())) {
          // Find the required parent class
          break;
        }

        // Continue to search up the inheritance tree
        currentClass = (TypeElement) typeUtils.asElement(superClassType);
      }
    }

    // Check whether the default public constructor is provided
    for (Element enclosed : classElement.getEnclosedElements()) {
      if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
        ExecutableElement constructorElement = (ExecutableElement) enclosed;
        if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers()
            .contains(Modifier.PUBLIC)) {
          // Find the default constructor
          return true;
        }
      }
    }

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

We add the isValidClass() method here to check if all our rules are met:

  • Must be a public class: classElement.getModifiers().contains(Modifier.PUBLIC)
  • Must be a non-abstract class: classElement.getModifiers().contains(Modifier.ABSTRACT)
  • Must be the implementation of a subclass or interface of the type specified by @Factoy.type(): First, we create an incoming Class(@Factoy.type()) element using elementUtils.getTypeElement(item.getQualifiedFactoryGroupName()). Yes, you can directly create TypeElement (using TypeMirror) by just knowing the legal class name. Next, let's check whether it's an interface or a class: superClassElement.getKind() == ElementKind.INTERFACE. So we have two cases here: if it's an interface, we judge classElement.getInterfaces().contains(superClassElement.asType()); if it's a class, we have to scan the inheritance hierarchy with currentClass.getSuperclass(). Note that the entire check can also be implemented using typeUtils.isSubtype().
  • Class must have an open default constructor: we traverse all closed elements classElement.getEnclosedElements(), and then check ElementKind.CONSTRUCTOR, Modifier.PUBLIC, and constructorElement.getParameters().size() == 0.

If all these conditions are met, isValidClass() returns true, or no, prints the error message and returns false.

Combining annotated classes

Once we check isValidClass() is successful, we will add FactoryAnnotated Class to the corresponding FactoryGrouped Classes, as follows:

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 messages are printed and exit the process
        }

        // All checks are OK, so you can add them.
        FactoryGroupedClasses factoryClass =
        factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
        if (factoryClass == null) {
          String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
          factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
          factoryClasses.put(qualifiedGroupName, factoryClass);
        }

        // If it conflicts with the id of other @Factory annotated classes,
        // Throw an IdAlreadyUsedException exception
        factoryClass.add(annotatedClass);
      } catch (IllegalArgumentException e) {
        // @ Factory.id() is empty - > Print error message
        error(typeElement, e.getMessage());
        return true;
      } catch (IdAlreadyUsedException e) {
        FactoryAnnotatedClass existing = e.getExisting();
        // Already exist
        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;
      }
    }
    ...
}

code generation

We have collected all the classes annotated by @Factory and saved them to FactoryAnnotated Classes and combined them into FactoryGrouped Classes. Now we will generate Java files for each factory:

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

Writing Java files is no different from writing other common files. Using the Writer object provided by Filer, we can connect strings to write our generated Java code. Fortunately, Square (which is famous for its many excellent open source projects) has provided us with JavaWriter This is an advanced library for generating Java code:

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

Note: Because JavaWriter is very popular, many processors, libraries and tools rely on JavaWriter. If you use dependency management tools, such as maven or gradle, this can lead to problems if a library-dependent version of JavaWriter is newer than other libraries. So I recommend that you copy and repackage JavaWiter directly into your annotation processor code (actually it's just a Java file).

Update: JavaWrite is now available JavaPoet Replaced.

Processing cycle

Annotation processing may occur more than once. The official javadoc definition process is as follows:

Annotation processing is an orderly cyclic process. In each loop, a processor may be required to process annotations in source and class files generated in the previous loop. The input to the first loop is the initial input to run the tool. These initial inputs can be seen as the output of the virtual 0th loop.

A simple definition: A processing loop is a process() method that calls an annotation processor. In our example of factory mode: FactoryProcessor is initialized once (not every loop creates a new processor object), however, if a new source file process() is generated, it can be called many times. Sounds strange, doesn't it? The reason is that these generated files may also contain @Factory annotations, which will also be processed by FactoryProcessor.

For example, our PizzaStore example will go through three cycles:

Round Input Output
1 CalzonePizza.java
Tiramisu.java
MargheritaPizza.java
Meal.java
PizzaStore.java
MealFactory.java
2 MealFactory.java --- none ---
3 --- none --- --- none ---

I explained that there is another reason for processing loops. If you look at our FactoryProcessor code, you will notice that we collect data and save them in a private domain Map < String, FactoryGrouped Classes > factoryClasses. In the first round, we detected Magherita Pizza, Calzone Pizza and Tiramisu, and then generated MealFactory.java. In the second round, MealFactory is used as input. Because the @Factory annotation was not detected in MealFactory, we expected it to be correct, but we got the following information:

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

The problem is that we did not clear factoryClasses, which means that in the second round of process(), the first round of data is still saved and the files that have been generated in the first round are attempted to generate, leading to this error. In our scenario, we know that only the class annotated by @Factory is checked in the first round, so we can simply fix this problem, as follows:

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

      // Clear factoryClasses
      factoryClasses.clear();

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

I know there are other ways to deal with this problem, such as we can also set a Boolean label. The key point is to remember that annotation processing takes many rounds of processing, and you can't overload or recreate the generated source code.

Separating Processors and Annotations

If you've seen us already Code base You will find that we organized our code into two maven modules. We do this because we want our factory model example users to compile annotations only in their projects, and include processor modules only for compilation. A little dizzy? Let's take an example, if we have only one bag. If another developer wants to use our Factory Mode Processor in his project, he must include the @Factory annotation and the entire Factory Processor code (including Factory Annotated Class and Factory Grouped Classes) into their project. I'm quite sure that he doesn't need to include processor-related code in his compiled project. If you're an Android developer, you've heard about the limitations of 65k methods (that is, in A. dex file, only 65000 methods can be addressed). If you use guava in FactoryProcessor and pack annotations and processors in a package, the Android APK installation package contains not only the code for FactoryProcessor, but also the entire guava code. Guava has about 20,000 methods. So it makes sense to separate annotations from processors.

Instantiation of generated classes

As you can see, in this PizzaStore example, the MealFactory class is generated, which is no different from other handwritten Java classes. Further, you need to instantiate it manually, just like other Java objects:

public class PizzaStore {

  private MealFactory factory = new MealFactory();

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

If you're an Android developer, you should also be familiar with one called Android. ButterKnife Annotation Processor. In ButterKnife, you annotate Android View with @InjectView. ButterKnife Processor generates a MyActivity $ViewInjector, but in ButterKnife you don't need to manually call new MyActivity $ViewInjector () to instantiate an object injected by ButterKnife, but instead use Butterknife.inject(activity). ButterKnife uses reflection mechanisms to instantiate MyActivity$$ViewInjector() objects:

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

But isn't the reflection mechanism slow? Will we use annotation processing to generate local code lead to a lot of reflection performance problems? Indeed, the performance of the reflection mechanism is a real problem. However, it does not need to create objects manually, and it does speed up the development of developers. ButterKnife has a hash table HashMap to cache instantiated objects. So MyActivity$$ViewInjector is instantiated only once using a reflection mechanism, and the second time MyActivity$$ViewInjector is needed, it is obtained directly from the hash table.

FragmentArgs It's very similar to Butter Knife. It uses reflection mechanisms to create objects without the need for developers to do so manually. FragmentArgs generates a special lookup table class when processing annotations, which is actually a hash table, so the entire FragmentArgs library only performs a reflection call the first time it is used. Once the parameter object of Fragemnt of Class.forName() is created, the following is run by local code.

As a developer of annotation processor, it's up to you to find a good balance between reflection and usability for other annotator users.

summary

So far, I hope you have a very deep understanding of the annotation process. I have to say it again: Annotation processors are a very powerful tool that reduces a lot of boring coding. I would also like to remind you that annotation processors can do much more complicated things than the factory model example I mentioned above. For example, generic type erasure, because the annotation processor occurs before type erasure. Here ) As you can see, when you write comment processing, there are two common problems you need to deal with: first, if you want to use ElementUtils, TypeUtils and Messager in other classes, you have to pass them in as parameters. Annotator I developed for Android AnnotatedAdapter In this article, I try to use Dagger (a dependency injection library) to solve this problem. Using it in this simple process sounds a bit overkill, but it does work well; the second problem is that you have to query Elements. As I mentioned earlier, processing Elements is like parsing XML or HTML. For HTML you can use jQuery, which is absolutely cool if you have a jQuery-like Kuna in the annotation processor. If you know of similar libraries, please let me know in the comments below.

Note that there are some flaws and pitfalls in FactoryProcessor code. I intentionally put these "errors" in order to demonstrate some common errors in the development process (such as "Attempt to recreate a file"). If you want to write your own annotation processors based on FactoryProcessor, don't copy and paste these traps directly. You should avoid them from the beginning.

Posted by felodiaz on Fri, 19 Apr 2019 11:48:33 -0700