This article was published simultaneously inCSDNShall not be reproduced without my permission

In the previous article, we implemented an example that mimics ButterKnife’s functionality using annotations + reflection. Given that reflection is done at run time, it somewhat affects program performance. Therefore, ButterKnife itself is not implemented based on annotations + reflection, but is processed at compile time using APT technology. APT what? Let’s see.

Introduction to APT

1. What is APT? APT is the Annotation Processing Tool. It is a javAC Tool, which means compile time Annotation processor in Chinese. APT can be used to scan and process annotations at compile time. Through APT, we can get the relevant information of annotations and annotated objects. After getting these information, we can automatically generate some code according to our needs, eliminating manual writing. Note that getting the annotations and generating the code is done at compile time, which greatly improves performance compared to reflection processing the annotations at run time. The core of APT is the AbstractProcessor class, which is described in more detail later.

2. Where is APT used?

APT technology is widely used in Java frameworks, including Android items and Java backend projects. In addition to ButterKnife mentioned above, APT technology is used in EventBus, Dagger2 and ARouter routing framework of Ari. So to understand and explore the implementation of these third party frameworks, APT is the one we must master.

3. How to build an APT project in Android Studio?

APT project needs to consist of at least two Java Library modules. It doesn’t matter, hand in hand to show you how to create a Java Library.First, create a New Android project, then File–>New–>New Module, open the panel shown above, and select Java Library. As mentioned earlier, an APT project should have at least two Java Library modules. So what are the roles of these two modules?

1. First you need an Annotation module, which is used to hold custom annotations.

2. In addition, you need a Compiler module, which depends on the Annotation module.

3. The App module and other business modules of the project need to rely on the Annotation module, and also need to rely on the Compiler module through annotationProcessor. The dependencies in Gradle of app module are as follows:

implementation project(':annotation')
annotationProcessor project(':factory-compiler')
Copy the code

The module structure of APT project is shown as follows:

Why is it important that these two modules be Java Libraries? If you create an Android Library module, you will find that AbstractProcessor class cannot be found because the Android platform is based on OpenJDK, which does not contain APT code. Therefore, when using APT, it must be done in the Java Library.

2. Start understanding APT from an example

You’ve all written examples of the simple factory pattern when you were learning Java basics. Think back to what a simple factory pattern is. To introduce an example of the factory pattern, first define a shape interface IShape and add the draw() method to it:

public interface IShape {
	void draw();
}
Copy the code

Next define several shapes to implement the IShape interface and overwrite the draw() method:

public class Rectangle implements IShape { @Override public void draw() { System.out.println("Draw a Rectangle"); } } public class Triangle implements IShape { @Override public void draw() { System.out.println("Draw a Triangle"); } } public class Circle implements IShape { @Override public void draw() { System.out.println("Draw a circle"); }}Copy the code

Next we need a factory class that takes a parameter and creates the corresponding shape based on the parameter we pass. The code looks like this:

public class ShapeFactory { public Shape create(String id) { if (id == null) { throw new IllegalArgumentException("id is  null!" ); } if ("Circle".equals(id)) { return new Circle(); } if ("Rectangle".equals(id)) { return new Rectangle(); } if ("Triangle".equals(id)) { return new Triangle(); } throw new IllegalArgumentException("Unknown id = " + id); }}Copy the code

Above is a simple factory pattern example code, presumably everyone can understand.

Now, the problem is that at any time during the development of the project, we can add a new shape. You will have to modify the factory class to accommodate the newly added shape. If we have to manually update the Factory class every time we add a shape class, does that affect our development efficiency? Wouldn’t it save us a lot of time if the Factory class could update the Factory code synchronously as we add new shapes?

What can be done to meet these requirements? As mentioned in section 1, using APT can help you generate code automatically. Can this factory class be automatically generated using APT technology? The only thing we need to do is add an annotation to the newly added shape class. The annotation handler will automatically generate ShapeFactory code at compile time based on the annotation information. Ideal is full, but reality is very skinny. It’s clear what to do, but we’re still a long way from the annotation handler generating code for us. However, let’s take the steps to implement the annotation handler and have it automatically generate the Factory class.

Use APT to handle annotations

We first add a Factory annotation under the annotation module. The Factory annotation has a Target of ElementType, indicating that it can annotate a class, interface, or enumeration. Retention is set to RetentionPolicy.class, indicating that it is valid in bytecode. The Factory annotation adds two members, a type of the Class type that represents the type of the annotated Class, and the same type that represents the same Factory. Let an ID of type String be used to represent the name of the annotated class. The Factory annotation code looks like this:

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

    Class type();

    String id();
}
Copy the code

Next we annotate the shape class with @Factory as follows:

@Factory(id = "Rectangle", type = IShape.class) public class Rectangle implements IShape { @Override public void draw() { System.out.println("Draw a Rectangle"); }}... Other shape class code analogs are no longer postedCopy the code

**2. Understand AbstractProcessor **

This brings us to the heart of this article. That’s right, AbstractProcessor! We create a FactoryProcessor class in the factory-compiler module that inherits from AbstractProcessor and overwrites the corresponding methods as follows:

@AutoService(Processor.class) public class FactoryProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); } @Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } @Override public SourceVersion getSupportedSourceVersion() { return super.getSupportedSourceVersion(); }}Copy the code

As you can see, the added @ AutoService annotation on the class, its function is used to generate the meta-inf/services/javax.mail annotation. Processing. The Processor file, That is when we were in the use of annotation handlers need to manually add the meta-inf/services/javax.mail annotation. Processing. The Processor, and after a @ AutoService it will be automatically generated for us. AutoService is a library developed by Google. To use AutoService, you need to add dependencies in the factory-compiler, as follows:

Implementation 'com. Google. Auto. Services: auto - service: 1.0 rc4'Copy the code

Next, if we look inside the FactoryProcessor class, we can see that there are four methods overridden in this class, from easy to difficult:

(1) public SourceVersion getSupportedSourceVersion()

This method is very simple, only one return value, is used to specify the current use of Java version, usually return SourceVersion. LatestSupported ().

(2) public Set<String> getSupportedAnnotationTypes()

The return value of this method is a Set collection, collection, to deal with the name of the annotation type of (there must be a complete package name + the name of the class, such as com. The example. The annotation. The Factory). Since you only need to handle the @Factory annotation in this case, all you need to do is add the name of @Factory to the Set.

(3) public synchronized void init(ProcessingEnvironment processingEnvironment)

This method is used to initialize the processor, and it takes a parameter of type ProcessingEnvironment, which is a collection of annotation processing tools. It contains a number of utility classes. For example, Filer can be used to write new files; Messager can be used to print error messages. Elements is a utility class that handles Elements.

Here it is important to know what Element is

In the Java language, an Element is an interface that represents a program Element that can refer to a package, class, method, or variable. The known subinterfaces of Element are as follows:

PackageElement represents a PackageElement. Provides access to information about packages and their members. Executableelements represent methods, constructors, or initializers (static or instance) of a class or interface, including annotation-type elements. A TypeElement represents a class or interface element. Provides access to information about types and their members. Note that an enumeration type is a class and an annotation type is an interface. VariableElement represents a field, enum constant, method or constructor parameter, local variable, or exception parameter.

Next, I want you to understand a new concept, which is to abandon our existing understanding of Java classes and think of them as structured files. What do you mean? Just think of a Java class as something like XML or JSON. With this concept in mind, it is easy to understand what Element is. With this concept in mind, look at the following code:

package com.zhpan.mannotation.factory; // PackageElement public class Circle { // TypeElement private int i; // VariableElement private Triangle triangle; // VariableElement public Circle() {} // ExecuteableElement public void draw( // ExecuteableElement String s) // VariableElement { System.out.println(s); } @Override public void draw() { // ExecuteableElement System.out.println("Draw a circle"); }}Copy the code

Is that clear now? Different types of Element actually map different class elements in Java! Knowing this concept will be a great help in understanding the rest of the code.

(4) public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

Finally, it’s time for the last and most important method in the FactoryProcessor class. The return value of this method is a Boolean that indicates whether the annotation is processed by the current Processor. If true is returned, these annotations are processed by this annotation and no subsequent Processor needs to process them; If false is returned, the annotations are not processed in this Processor, and subsequent processors can continue to process them. In the body of this method, we can verify that the object being annotated is valid, write code to process the annotations, and automatically generate the required Java files. This method is therefore the most important method in AbstractProcessor. Most of the logic we’re going to deal with is done in this method.

With these four methods in mind, we can write the FactoryProcessor class as follows:

@AutoService(Processor.class) public class FactoryProcessor extends AbstractProcessor { private Types mTypeUtils; private Messager mMessager; private Filer mFiler; private Elements mElementUtils; private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>(); @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mTypeUtils = processingEnvironment.getTypeUtils(); mMessager = processingEnvironment.getMessager(); mFiler = processingEnvironment.getFiler(); mElementUtils = processingEnvironment.getElementUtils(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(); annotations.add(Factory.class.getCanonicalName()); return annotations; } @Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {for (Element annotatedElement) roundEnv.getElementsAnnotatedWith(Factory.class)) { } return false; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }}Copy the code

The FactoryProcessor code. In the process approach through roundEnv getElementsAnnotatedWith (Factory. Class) method have been given a collection of elements are annotated. Normally, this collection would contain all the factory-annotated Shape elements, which would be a TypeElement. However, when writing program code, some new colleagues may not understand the purpose of @Factory and mistakenly use @Factory on an interface or abstract class, which is not in line with our standards. Therefore, you need to determine in the Process method whether the element annotated by @Factory is a class or not, and if not, throw an exception to terminate the compilation. The code is as follows:

@Override public boolean process(Set<? extends TypeElement> annotations, For (Element annotatedElement: Element annotatedElement); roundEnv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() ! = ElementKind.CLASS) { throw new ProcessingException(annotatedElement, "Only classes can be annotated with @%s", Factory.class.getSimpleName()); } TypeElement typeElement = (TypeElement) annotatedElement; FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement); . } return true; }Copy the code

Based on object-oriented thinking, we can wrap the information contained in annotatedElement into an object for later use, so we can declare a separate FactoryAnnotatedClass to parse and store the information about The annotatedElement. FactoryAnnotatedClass code looks like this:

public class FactoryAnnotatedClass { private TypeElement mAnnotatedClassElement; private String mQualifiedSuperClassName; private String mSimpleTypeName; private String mId; public FactoryAnnotatedClass(TypeElement classElement) { this.mAnnotatedClassElement = classElement; Factory annotation = classElement.getAnnotation(Factory.class); mId = annotation.id(); if (mId.length() == 0) { 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())); -qualifiedTypename try {Class<? > clazz = annotation.type(); mQualifiedSuperClassName = clazz.getCanonicalName(); mSimpleTypeName = clazz.getSimpleName(); } catch (MirroredTypeException mte) {// The class did not compile. DeclaredType classTypeMirror = (DeclaredType) mte.gettypemirror (); TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement(); mQualifiedSuperClassName = classTypeElement.getQualifiedName().toString(); mSimpleTypeName = classTypeElement.getSimpleName().toString(); }} / /... Save the getter}Copy the code

In order to generate the required ShapeFactory class, before generating the ShapeFactory code, we need to carry out a series of verification on the elements annotated by the Factory. Only after the verification, the ShapeFactory code can be generated. Based on the requirements, we list the following rules:

1. Only classes can be annotated by @Factory. Since we need to instantiate Shape objects in ShapeFactory, the @Factory annotation declares Target as elementType.type, but the interface and enumeration do not meet our requirements. 2. Classes annotated by @Factory need to have public constructors in order to instantiate objects. 3. The annotated class must be a subclass of the class specified by type 4. Id must be of type String and must be unique in the same type group 5. Annotation classes of the same type are generated in the same factory class

According to the above rules, we will complete the verification step by step, as follows:

private void checkValidClass(FactoryAnnotatedClass item) throws ProcessingException { TypeElement classElement = item.getTypeElement(); if (! classElement.getModifiers().contains(Modifier.PUBLIC)) { throw new ProcessingException(classElement, "The class %s is not public.", classElement.getQualifiedName().toString()); } / / if it is ABSTRACT method is throwing an exception cancel compile the if (classElement. GetModifiers (). The contains (Modifier. The ABSTRACT)) {throw new ProcessingException(classElement, "The class %s is abstract. You can't annotate abstract classes with @%", classElement.getQualifiedName().toString(), Factory.class.getSimpleName()); } // This class must be a subclass of the class specified in @factory.type (), Otherwise, throw an exception to terminate compile TypeElement superClassElement = mElementUtils. GetTypeElement (item) getQualifiedFactoryGroupName ()); If (superClassElement getKind () = = ElementKind. INTERFACE) {/ / check whether are annotated class implementation or inherited @ Factory. The type (s) specified types, here are all IShape if (! classElement.getInterfaces().contains(superClassElement.asType())) { throw new ProcessingException(classElement, "The class %s annotated with @%s must implement the interface %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); } } else { TypeElement currentClass = classElement; while (true) { TypeMirror superClassType = currentClass.getSuperclass(); If (superClassType. GetKind () == Typekind.none) {// If (superClassType. GetKind () == TypeKind. Throw new ProcessingException(classElement, "The class %s annotated with @%s must inherit from %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); } the if (superClassType. ToString (). The equals (item) getQualifiedFactoryGroupName ())) {/ / check through, terminate the traversal break; } currentClass = (TypeElement) mTypeUtils.asElement(superClassType); }} // Check whether public enclosed constructor for (Element enclosed: classElement.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructorElement = (ExecutableElement) enclosed; if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers().contains(Modifier.PUBLIC)) { // There is a public no-parameter constructor, check the end return; }}} // Throw an exception when it detects a public constructor with no parameters, Throw New ProcessingException(classElement, "The class %s must provide an public empty default constructor", classElement.getQualifiedName().toString()); }Copy the code

If the above validation passes, then the class annotated by @Factory is the right class for us, and we can then process the annotation information to generate the required code. But in object-oriented thinking, we need to declare FactoryGroupedClasses to hold the FactoryAnnotatedClass, and in this class we do the code generation for the ShapeFactory class. FactoryGroupedClasses code:

public class FactoryGroupedClasses {

    private static final String SUFFIX = "Factory";
    private String qualifiedClassName;

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

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert) {
        FactoryAnnotatedClass factoryAnnotatedClass = itemsMap.get(toInsert.getId());
        if (factoryAnnotatedClass != null) {
            throw new IdAlreadyUsedException(factoryAnnotatedClass);
        }
        itemsMap.put(toInsert.getId(), toInsert);
    }

  public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        //  Generate java file
        ...
    }
}
Copy the code

Next, add all FactoryGroupedClasses to the collection

   private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>();

	// 	...
	FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
    if (factoryClass == null) {
            String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
            factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
            factoryClasses.put(qualifiedGroupName, factoryClass);
      }
	factoryClass.add(annotatedClass);
	// ...
Copy the code

OK! So far, all the preparations have been completed. The next step is to generate the ShapeFactory class based on the annotation information. Excited? Iterate over the set of factoryClasses and call the generateCode() method of the FactoryGroupedClasses class to generateCode:

for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
          factoryClass.generateCode(mElementUtils, mFiler);
     }
Copy the code

However, when we remove the generateCode(mElementUtils, mFiler) method….. What? It’s still an empty method, we haven’t implemented it yet! Laugh cry 😂…

Four, understand JavaPoet and use it to generate ShapeFactory class

At this point, our only remaining requirement is to generate the ShapeFactory class. In the last section we got the Filer in the Init (ProcessingEnvironment ProcessingEnvironment) method of the FactoryProcessor class, We also mentioned that Filer can be used to write files, that is, we can use Filer to generate the ShapeFactory class we need. However, using Filer directly requires you to concatenate the class code manually, and it’s possible to accidentally write the wrong letter and make the generated class invalid. Therefore, we need to take a look at the JavaPoet library. JavaPoet is square’s open source JavaPoet framework, written by Jake Wharton. JavaPoet can use objects to help us generate class code, that is, we can simply wrap the generated class file into an object, JavaPoet can automatically generate the class file for us. The use of this library will not be explained in detail here, there is a need to know can be viewed on Github, it is very simple to use.

Here’s the code to build and automatically generate the ShapeFactory class using JavaPoet:

public void generateCode(Elements elementUtils, Filer filer) throws IOException { TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName); String factoryClassName = superClassName.getSimpleName() + SUFFIX; String qualifiedFactoryClassName = qualifiedClassName + SUFFIX; PackageElement pkg = elementUtils.getPackageOf(superClassName); String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString(); MethodSpec.Builder method = MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .addParameter(String.class, "id") .returns(TypeName.get(superClassName.asType())); method.beginControlFlow("if (id == null)") .addStatement("throw new IllegalArgumentException($S)", "id is null!" ) .endControlFlow(); for (FactoryAnnotatedClass item : itemsMap.values()) { method.beginControlFlow("if ($S.equals(id))", item.getId()) .addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString()) .endControlFlow(); } method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = "); TypeSpec typeSpec = TypeSpec .classBuilder(factoryClassName) .addModifiers(Modifier.PUBLIC) .addMethod(method.build()) .build(); JavaFile.builder(packageName, typeSpec).build().writeTo(filer); }Copy the code

Ok, now the project is ready to automatically generate the Java files we need. Now to verify this, Build the project, go to Project mode, App –>build–>generated–>source–>apt–>debug–>(package)–>factoryThis class is not written by ourselves, but is generated automatically by using APT’s series of SAO operations. Now you can add another shape-class to implement IShape and attach the @Factory annotation, which will automatically be generated in the ShapeFactory when compiled again!

That’s the end of this article. I believe that after reading this article must be a great gain, because after mastering APT technology, and then to study the use of APT third party framework source code, will be with ease, twice the result with half the effort.

Because this article is more complex and has more code, the source code for the project has been placed at the end of the article for reference.

Download the source code

The resources

Java annotation handler

The JDK documentation AbstractProcessor

Good library recommendation

I recommend BannerViewPager. This is a ViewPager based on the implementation of the powerful infinite round cast library. BannerViewPager can realize Banner style and indicator style for Tencent Video, QQ Music, Kugou Music, Alipay, Tmall, Taobao, Youku Video, Himalaya, netease Cloud Music, Bilibili and other apps.

Welcome to BannerViewPager on Github!