This is the 8th day of my participation in the August More Text Challenge

APT&JavaPoet practice

APT:

The full name of APT is Annotation Processing Tool, which is a Tool provided by Java to scan and process annotations during code compilation. It will scan and check source code files to find annotations, and then clients can customize Annotation Processing methods. After obtaining relevant annotations, Get some information from the annotations to implement additional code processing.

In addition to scanning and parsing annotations, APT can also insert logic to generate new source code files based on annotations in the process of processing annotations, and finally, compile the newly generated code files with the original code files.

APT works as shown in the following figure: During compilation, APT tool scans annotations to generate new Java source files, which will be compiled together with the original source files and finally get bytecode files.

APT works like this: During compilation of Java code, the compiler examines subclasses of AbstractProcessor, looks for an annotation handler, and calls the process method of the subclass annotation handler to add all elements (classes, interfaces, member variables……) with custom annotations. Are passed to the Process method so that the client can take these elements in the process method and generate the corresponding code file from them.

Projects that include APT typically work in three modules, the pure Java annotation declaration library and annotation processing library, and the actual business modules that rely on these two modules. The diagram below:

APT is widely used. Well-known frameworks on Android, such as ButterKnife, EventBus, Dagger2 and ARouter, all use APT technology.

AbstractProcessor:

Each custom annotation handler needs to inherit from the abstract base class and override the abstract method process. In addition, the following methods are generally overwritten:

// This method is called automatically, and the processingEnvironment passed in represents the annotation processor's working environment
// With this parameter, we can get some useful tools for processing
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    // Filer object to create a new file-related utility class
    mFiler = processingEnv.getFiler();
    // Elements, the tool used to process Element
    mElementUtils = processingEnv.getElementUtils();
    // Messager object, mainly provides some log output related API
    mMessager = processingEnv.getMessager();
    // The Types object, a tool for handling TypeMirror
    mTypeUtils = processingEnv.getTypeUtils();
}
// Annotate the actual working logic of the processor to add custom logic for scanning, detecting, processing, and generating new Java code
// Two input parameters:
// The first argument is a set that declares the annotations to be processed
// The second argument is to get information about the element annotated by the specified annotation
// Return value: indicates whether the corresponding annotation has been processed by the annotation handler
// return true, indicating that the annotation handler has already processed the corresponding annotation and other handlers will not process the annotation
// return false, indicating that the annotation handler did not process the corresponding annotation, and subsequent handlers can continue processing
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    return true;
}
// Specify which annotations need to be registered by the annotation handler
@Override
public Set<String> getSupportedAnnotationTypes(a) {
    Set<String> types = new HashSet<>();
    types.add(Router.class.getCanonicalName()); // Returns the full class name of the annotation
  return types;
}
// Specify the version of Java you are using, usually using latestSupported unless you specify which version you must support
// I print back RELEASE_8
@Override
public SourceVersion getSupportedSourceVersion(a) {
    return SourceVersion.latestSupported();
}
Copy the code

Element and TypeMirror were mentioned above. To understand the concept of Element, you need to understand it based on structured files. A typical example of a structured storage format such as XML is as follows:

<employe>
<name>Aaron</name>
<city>London</city>
<age>20</age>
</employe>
Copy the code

For this piece of XML code, we can write and read the employee’s information according to the specified nodes. The basic elements of an employee include name, city, and age. The file above is structured to store the employee’s name, city of work, and age.

To the Java compiler, the structure of elements in code is actually fixed. The basic elements that make up the code: packages, classes, functions, fields, variables, code execution statements, etc. Java defines a base class for these elements called Element.

A simple Piece of Java code can be thought of as consisting of these basic elements:

    package me.aaron.apt;  // PackageElement
    public class Test {  // TypeElement
        private int i; // VariableElement
        private Triangle triangle;  // VariableElement
        public Test(a) {} // ExecuteableElement
        public void draw( // ExecuteableElement String s)   // VariableElement
        { System.out.println(s); }}Copy the code
  • PackageElement represents a PackageElement. Provides access to information about packages and their members.

  • Executableelements represent the methods, constructors, or initializers (static or instance) of a class or interface, including annotation type elements.

  • TypeElement represents a class or interface element. Provides access to information about types and their members. Note that an enumerated type is a class (enum compilation ultimately yields the class product), whereas an Annotation type is an interface (an Annotation is essentially an interface that inherits from an Annotation).

  • VariableElement represents a field, enum constant, method or constructor parameter, local variable, or exception parameter.

  • TypeParameterElement represents a generic parameter in a class, interface, method, or constructor.

These element types provide corresponding methods to obtain some information.

Here’s an example of one of the core elements: TypeElement

Provides a canonical name for this element such as getQualifiedName (for example, me.aarom.apt.Test is the canonical name for the Test example above), GetSuperclass gets the immediate parent of the element type (NoType if the element is an interface or java.lang.Object), such as getInterfaces, which get the interface of this implementation (and return its parent if it is an interface).

More apis can be found in the DOC documentation, which establishes the idea that we can use these elements to retrieve information we need for annotation processing.

And then another concept of TypeMirror. Notice that getInterfaces, getSuperclass, return TypeMirror related objects that represent the concepts of types in the Java language, including primitive types, declarative types (classes, interfaces), Array type, null type……

So many concepts introduced, let’s look at a specific demo.

APT practices:

Pure Java, no Android involved.

Step1: create the annotation module

Create a New Java Library for router_Annotation, which must be a Java Library. And the code that you put in there is very simple, it’s an Annotation.

I won’t introduce you to annotations.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Router {
    String value(a) default "" ;
}
Copy the code

I’ve defined a Router annotation that will eventually be annotated on the Activity. The value represents the protocol name of the currently annotated Activity in the routing table.

There are also some build.gradle configurations that are automatically done by AS and generally do not need to be handled.

Step2: Create the annotation handler Module

Create a new Router_apt annotation handler Module, also a pure Java Library. The core of this is our custom annotation handler that inherits from AbstractProcess.

The code is as follows:

@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {
     @Override
     public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // ...
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // This is where code generation is done.
    }
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> types = new HashSet<>();
        types.add(Router.class.getCanonicalName()); // Returns the full class name of the annotation
  return types;
    }
    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        returnSourceVersion.latestSupported(); }}Copy the code

Note that we used an @autoService annotation for the RouterProcessor to automatically generate meta-INF configuration information for the current annotation processor. This annotation requires additional dependencies. If you do not use the @AutoService annotation, We need to manually configure the META-INF information ourselves, which is the premise for our custom annotation handler to be called.

The meta-INF contains some configuration information of our JAR package. When javac is compiled, it will look for subclasses that implement AbstractProcessor in the META-INF jar package and call them. In much the same way that an Activity in Android needs to be registered in its Manifest file to work.

The final build.gradle for this module looks like this:

apply plugin: 'java-library'
dependencies {
    implementation fileTree(dir: 'libs' , include: [ '*.jar' ])
    implementation project( ':router_annotation' )
    // A framework to help us generate Java code
    implementation 'com. Squareup: javapoet: 1.12.1'
    implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2'
}
sourceCompatibility = "Seven"
targetCompatibility = "Seven"
Copy the code

Generally speaking, after completing these two steps, we can happily handle the logic of code generation in processor. However, there is a very frustrating point that I failed to successfully generate code when writing demo. The reasons are as follows:

Gradle-wrapper and gradle-plugin need to be reduced to 4.10.1 and 3.2.0 respectively

Reason: The javaCompileTask task in Gradle-3.3-3.4 does not process the Annotation Processer. As a result, the annotation handler will not be executed during our build.

JavaPoet:

In the annotation processor, we can get some elements related to custom annotations. This is the first part of the work. Then, we need to parse these elements to get the information we want, and use this information to help us generate Java code.

The essence of generating code is to create a *.java file and write code into it, so there are many ways to generate code.

In the case of our routing framework, we parse the information needed by the routing table, and then generate a piece of code to add a single piece of routing information to the routing table, like this:

Simple code like this, in fact, create a file and write, but if the generated code logic is complex, this time needs to deal with the work is more troublesome, we may need to deal with the work is: Package name handling, import dependencies, class inheritance, interface implementation, class modifiers, method names, method parameters, method return values, method modifiers…… There is a lot of work to deal with.

So to simplify this part of the job, we found a tool called JavaPoet. It is an open source framework that provides a Java Api and is dedicated to generating Java source files.

The following concepts are somewhat similar to the concepts of Java structuring.

Let’s take a look at what the key classes provided by JavaPoet correspond to in Java:

JavaFile The Java file used to construct output containing a top-level class is an abstraction from a. Java file.
TypeSpec Used to generate classes, interfaces, or enumerations. It’s an abstraction of these guys.
MethodSpec Used to generate a constructor or method, is an abstraction of a function.
FieldSpec Used to generate fields and is an abstraction of fields.
ParameterSpec Used to generate method parameters and is an abstraction of parameters.
AnnotationSpec Used to create annotations, is an abstraction of annotations.

Each of these key classes is responsible for generating the corresponding “objects” and converting them into the corresponding code when the Java code is finally generated. Also to help with generating code (because the files are written to characters, JavaPoet needs to know whether you are writing, for example, a member variable called localVariable or a string called “localVariable”), JavaPoet provides the following placeholders:

$L Literal substitution (direct substitution)
$S String substitution
$T Type substitution
$N The name of the replacement

What does that mean?

// The following example defines a method
MethodSpec method = MethodSpec.methodBuilder("method")
    .addStatement("$T file;", File.class)            // File file;
    .addStatement("$L = null;"."file")              // file = null;
    // file = new File("~/fileName");
    .addStatement("file = new File($S);"."~/fileName")
    // return file.getName();
    .addStatement("return $L.getName();"."file")
    .returns(String.class)
    .build();
// This defines a mothod method. If the generated code needs to call this method, it can be replaced with a name:
// Let's define a new invokeMethod method:
MethodSpec invokeMethod = MethodSpec.methodBuilder("invokeMethod")
    .addStatement("$N();",method)  // method();
    .returns(void.class)
    .build();
Copy the code

Take the mapping code above as an example:

The above code is only the most simple mapping of the Activity routing table. There is still a lot of work to do before a complete routing framework needs annotation processor, such as support Service, BroadCastReceiver mapping, some routing parameters can be format validation, etc. For example, the annotation processor module can also be programmed to make it more orderly (such as unified management of constants).

Build. Gradle = build.gradle = build.gradle = build.gradle = build.gradle

javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]      
    }
}  
// It is then available in the annotation handler
String moduleName = processingEnv.getOptions().get("moduleName") 
Copy the code

conclusion

Through APT and Java code generation, we completed the most basic work of the routing framework: the establishment of route mapping. RouterMapping$$XXX saves the Uri of the module and the actual path of the Activity in the RouterMapping$$XXX object of each module. Then we call the mapping method of these generated objects and output the mapping to the Map object. A routing table is established.