I have submitted the code in this article to GitHub. Download the source code on demand from —->

preface

In the Android Annotations series, Annotations (ii), I briefly introduced the basic use and definition of annotations. It also raises the following questions: when we declare an annotation, do we need to manually find all Class objects or fields or methods? How do you generate a new class definition through annotations? When faced with these questions, I’m sure your first thought is, “Is there a tripartite library? Does Java provide libraries or methods to do this?” Of course, Java certainly provides us with the APT tool that we are both unfamiliar and familiar with.

Why do I say unfamiliar and familiar here? I believe that for most Android applications, we use some of the major libraries, such as Dagger2, ButterKnife, EventBus, etc., which use APT technology. If the big boys are using it, why don’t we learn about it? Ok, back to the book, let’s look at how to use APT to deal with the problems we mentioned earlier.

APT Technology Introduction

Before the specific understanding of APT technology, it is briefly introduced. APT(Annotation Processing Tool) is a Tool provided by JavAC to scan and process annotations at compile-time. It checks source code files and finds annotations, and then performs additional Processing according to user-defined Annotation Processing methods. APT not only parses annotations, but also generates other source files based on annotations, eventually compiling the new source file with the original one (note: APT does not modify the source file, but only generates new files, such as adding methods to an existing class). The specific flow chart is as follows:

APT technology usage rules

The use of APT technology requires us to abide by certain rules. Let’s take a look at a rule diagram of the entire APT project, as shown below:

APT usage dependency

From the figure, we can see that the construction of the whole APT project needs three parts:

  • Annotation handler library (including our annotation handler)
  • Annotation declaration library (annotations used to store declarations)
  • Android/Java projects that actually use APT

And the dependency of the three parts is annotation processing tools rely on annotation library, Android/Java projects rely on annotation processing tools library and annotation library at the same time.

Why separate the annotation processor into a library?

Android projects do not include APT classes by default. So to use APT, you have to create a Java Library. For Java projects, a separate library is easier to maintain and extend.

Why split annotation declarations into a separate library instead of the annotation processing tool library?

, for example, if the annotation declaration and annotation handlers for the same library, if developers hope to put our annotation processor used in his project, then it must contain annotation declaration and the annotation processor code, we can sure is that he does not want to have compiled project contains the processor related code. He just wants to use our annotations. So it makes sense to separate annotation handlers and annotations into a library. The following articles will specifically describe ways to keep our annotation handlers unpackaged in our actual projects.

Annotation processor declaration

Now that we know how to use ATP, let’s take a look at how to declare an annotation processor. Each annotation processor needs to incorporate the AbstractProcessor class, as shown below:

class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {}
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

    @Override
    public Set<String> getSupportedAnnotationTypes() {}}Copy the code
  • init(ProcessingEnvironment processingEnv)This method is called when each annotation handler is initialized, passing in the ProcessingEnvironment parameter. ProcessingEnvironment provides a number of useful utility classes, Elements, Types, and Filer. We’ll see more about this later.
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)This is where you can write your code to scan and process annotations and generate Java files. The RoundEnvironment parameter allows you to query for annotated elements that contain specific annotations, as we’ll see later.
  • getSupportedAnnotationTypes(): returns the type of annotation handled by the current annotation handler as a collection of strings. Where the string is the annotation that the processor needs to processLegal name.
  • getSupportedSourceVersion(): specifies the version of Java you are using, usually returned hereSourceVersion.latestSupported(). If you have a good reason to specify a Java version, you can return SourceVersion.RELAEASE_XX. But the former is recommended.

In Java1.6 version provides SupportedAnnotationTypes and SupportedSourceVersion two annotations to replace getSupportedSourceVersion getSupportedAnnotationType S two methods, that is:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"Name of a legal note."})
class MineProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false; }}Copy the code

Here is need to be aware of the above mentioned two annotations are new JAVA 1.6, so for reasons of compatibility, suggest or direct rewriting getSupportedSourceVersion () and getSupportedAnnotationTypes () method.

Register annotation handlers

Now that we have a basic understanding of processor declarations, we might be wondering, how do I register an annotation handler with the Java compiler? You have to provide a.jar file, just like any other.jar file, you need to package your annotation handler into this file, and inside your JAR, . You need to pack to a particular file javax.mail annotation. Processing. The Processor to the meta-inf/services directory. Like this:

Meta-inf/Services is an information package. The files and directories in the directory are approved and interpreted by the Java platform to configure applications, extenders, class loaders, and service files, which are generated automatically when the JAR is packaged

Including javax.mail. The annotation. Processing. The contents of the Processor file for each annotation Processor legal full name list, each element line segmentation, which is like the following:

com.jennifer.andy.processor.MineProcessor1
com.jennifer.andy.processor.MineProcessor2
com.jennifer.andy.processor.MineProcessor3
Copy the code

Finally, we as long as you are generated. Jar in your buildPath, then the Java compiler will automatically check and read the javax.mail annotation. Processing. The contents of the Processor, and register the annotation Processor.

Of course, for our current compilers, such as IDEA, AndroidStudio, etc., we just create the corresponding files and folders and put them in buildPath instead. The reason, of course, is that the compiler handles it for us. If you still trouble, that we can use Google to provide us with AutoService annotations Processor, used to generate the meta-inf/services/javax.mail annotation. Processing. The Processor file. That is, we can use it like this:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"Name of a legal note."})
@AutoService(Processor.class)
class MineProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false; }}Copy the code

All we need to do is declare @autoService (processor.class) on the class, and we don’t need to worry about anything else. Is it convenient? (of course use AutoService you need to add in Gralde depend on the compile ‘com. Google. Auto. Services: auto – service: 1.0 rc2’).

Annotation processor scan

During annotation processing, we need to scan all the Java source files. Each part of the source code is a specific type of Element, meaning that Element represents elements in the source file, such as packages, classes, fields, methods, and so on. The overall relationship is shown in the figure below:

  • Parameterizable: represents elements of mixed types (not just elements of one type)
  • TypeParameterElement: a class, interface, method, or constructor with generic parameters.
  • VariableElement: Represents a field, constant, method, or constructor. Parameter, local variable, resource variable, or exception parameter.
  • QualifiedNameable: An element with a qualified name
  • ExecutableElement: A method, constructor, or initializer (static or instance) that represents a class or interface, including annotation type elements.
  • TypeElement: Represents classes and interfaces
  • PackageElement: indicates a package

Then we will analyze it in detail through the following examples:

package com.jennifer.andy.aptdemo.domain; //PackageElement class Person {//TypeElement private Stringwhere; //VariableElement public voiddoSomething() { }//ExecutableElement
    
    public void run() {//ExecutableElement int runTime; //VariableElement } }Copy the code

From the above examples, we can see that APT scans the whole source file. It’s kind of like parsing XML files (structured text).

Since the source file is structured data when scanned, can we get the parent and child elements of an element? . Of course we can. For example, if we have a TypeElement that is public Class Person, we can iterate over all of its children.

TypeElement person= ... ;  
for(Element e: person. GetEnclosedElements ()) {/ / traverse its child Element parent = um participant etEnclosingElement (); // Get the nearest parent of the child element}Copy the code

GetEnclosedElements () and getEnclosingElement() are interface declarations in Element. For more information, please check the source code.

Element type judgment

Now that we know the classification of the Element Element, we can see that Element sometimes stands for more than one Element. For example, TypeElement represents a class or interface. What are the specific differences? Let’s move on to the following example:

public class SpiltElementProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment RoundEnvironment) {// This is done by getting all elements that contain Who annotationssetSet the Set <? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);for (Element element : elements) {
            if(element.getKind() == elementkind.class) {// If the element is a CLASS}else if(elementkind.interface) {// If the current element is an INTERFACE}}return false; }... Omit some code}Copy the code

In the example above, we through roundEnvironment. GetElementsAnnotatedWith (Who) class) for all contain @ Who annotate element in the source file, by calling the element. The getKind () specifically determine the current element types, The specific element type is the ElementKind enumeration type. The ElementKind enumeration declaration is as follows:

Enumerated type species
PACKAGE package
ENUM The enumeration
CLASS class
ANNOTATION_TYPE annotations
INTERFACE interface
ENUM_CONSTANT Enumerated constants
FIELD field
PARAMETER parameter
LOCAL_VARIABLE The local variable
EXCEPTION_PARAMETER Exception parameters
METHOD methods
CONSTRUCTOR The constructor
OTHER other
Omit… Omit…

Element type judgment

Then you have a problem, since we’re scanning for fetched elements that represent structured data in the source file. So what if we want to get more information about elements? For example, for a CLASS, we now know that it is an elementKind. CLASS CLASS, but how do I get information about its parent? We also know that a METHOD is of elementKind. METHOD type. How do I obtain the return value type, parameter type, and parameter name of this METHOD?

Of course, Java already provides methods for this. These problems can be solved using the Mirror API, which allows us to view method, field, and type information in uncompiled source code. In practice, the element type is obtained via TypeMirror. Look at the following example:

public class TypeKindSpiltProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if(element.getkind () == elementkind.method) {// If the current element is an ExecutableElement methodElement = (ExecutableElement) element; TypeMirrorreturnType = methodElement.getReturnType(); // Get TypeMirror TypeKind kind =returnType.getKind(); // Get the element type system.out.println ("print return type----->"+ kind.toString()); }}return false; }}Copy the code

Looking at the code above, we can see that when we use the annotation handler, we find the corresponding Element first. If you want more information about that Element, you can use TypeKind with TypeMirror to determine the current Element type. Of course, the TypeMirror method may differ for different types of Elements. TypeKind enumeration declarations are shown in the following table:

Enumerated type type
BOOLEAN Boolean type
BYTE Byte type
SHORT Short type
INT The int type
LONG long
CHAR The type char
FLOAT A float
DOUBLE Type double
VOID Void, used primarily for the return value of a method
NONE No type
NULL Empty type
ARRAY An array type
Omit… Omit…

Element visibility modifier

In the annotation processor, we can not only get the type and information of the element, but also the visibility modifiers (such as public, private, and so on) for that element. You can call the Element.getModifiers() directly as follows:

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if(element.getKind() == elementkind.class) {// If the element is a CLASS Set<Modifier> modiFIERS = element.getModifier (); // Get the visibility modifierif(! Contains (mysql.public)) {// If the current class is not PUBLIC throw new ProcessingException(classElement,"The class %s is not public.", classElement.getQualifiedName().toString()); }}return false; }}Copy the code

Modifer is an enumeration type in the above code, the enumeration is as follows:

public enum Modifier {

    /** The modifier {@code public} */          PUBLIC,
    /** The modifier {@code protected} */       PROTECTED,
    /** The modifier {@code private} */         PRIVATE,
    /** The modifier {@code abstract} */        ABSTRACT,
    /**
     * The modifier {@code default}
     * @since 1.8
     */
     DEFAULT,
    /** The modifier {@code static} */          STATIC,
    /** The modifier {@code final} */           FINAL,
    /** The modifier {@code transient} */       TRANSIENT,
    /** The modifier {@code volatile} */        VOLATILE,
    /** The modifier {@code synchronized} */    SYNCHRONIZED,
    /** The modifier {@code native} */          NATIVE,
    /** The modifier {@code strictfp} */        STRICTFP;
}

Copy the code

Error handling

In the annotation processor customization, we can not only call the relevant methods to get the element information in the source file, but also report errors, warnings, and prompts through the Messager provided by the processor. Can be used directly processingEnv. GetMessager (.) printMessage (Diagnostic. Kind. NOTE, MSG); It is important to note that it is not a logging tool for processor development, but is used to write messages to third-party developers using the annotation library. That is, if we throw an exception like a traditional Java application, the JVM running the annotation processor will crash, and information about errors in the JVM is not very friendly to third-party developers, so using Messager is recommended and highly recommended. As shown below, when we determine that a class is not a public modifier, we use Messager to report the error.

The annotation processor is running in its own virtual machine JVM. Yes, you read that right, Javac starts a full Java virtual machine to run the annotation processor.

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if(element.getKind() == elementkind.class) {// If the element is a CLASS Set<Modifier> modiFIERS = element.getModifier (); // Get the visibility modifierif(! Modifiers. The contains (Modifier. PUBLIC)) {/ / if the current class is not PUBLIC roundEnvironment. GetMessager () printMessage (Diagnostic. Kind. The ERROR,"the class is not public"); }}return false; }}Copy the code

At the same time, different message levels are described in the official documentation. You can view more message levels from the diagnostic. Kind enumeration.

Error information is displayed

If you need to print logs using Messager provided by the processor, you need to view the output information in the following screen:

 roundEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "the class is not public");
Copy the code

Using the above code, the log view interface is as follows:

Every time you compile the code, if you use Messager to print the log, it will be displayed.

Files are generated

Up to now, we have basically understood the basic knowledge of APT. Now let’s talk about how APT technology generates new class definitions (that is, create new source files). For creating a new file, we do not call IO streams for read and write operations as we do for basic file operations. Instead, the source files are constructed using JavaPoet. (when you use JavaPoet, of course, you need to add in gradle depend on the compile ‘com. Google. Auto. Services: auto – service: 1.0 rc2’), the use of JavaPoet is also very simple, as follows:

JavaPoet can be useful for generating source files when annotating or interacting with metadata files (for example, database schemas, protocol formats). By generating code, you eliminate the need to write templates while maintaining a single source of metadata.

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.jennifer.andy.apt.annotation.Who")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CreateFileByJavaPoetProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        createFileByJavaPoet(set, roundEnvironment);
        return false; Private void createFileByJavaPoet(Set<? Extends TypeElement> Annotations, RoundEnvironment roundEnv) {MethodSpec main = MethodSpec. MethodBuilder ("main") .addModifiers(Modifier.PUBLIC, STATIC)// Set the visibility Modifier to public static. returns(void. Class)// Set the return value to void."args"AddStatement (args.addStatement)// Add an array of parameters of type String."$T.out.println($S)", System.class, "Hello, JavaPoet!"// Add statement.build(); TypeSpec helloWorld = TypeSpec. ClassBuilder ("HelloWorld").addModifiers(Modifier.public, Modifier.final).addMethod(main)// Add main method to HelloWord class.build (); JavaFile = javafile.builder (JavaFile = javafile.builder ("com.jennifer.andy.aptdemo.domain", helloWorld) .build(); Try {// Create file javafile.writeto (processingenv.getFiler ()); } catch (IOException e) {log(e.getMessage()); }} /** * call print statement */ private voidlog(String msg) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg); }}Copy the code

After we build the above code, we can get the following files in our build directory:

For more details on how to use JavaPoet, you can refer to the official documentation ——–>JavaPoet

Separate the processor from the project

In the APT usage rules described above, we divide the annotation declaration library and annotation processor library into two libraries. I have also explained the specific reasons in detail. Now let’s consider the following questions. Even if we split the two libraries into two separate libraries, if a developer wants to use our custom annotation handler for his project, his entire project compilation must also include the annotation handler and annotation declaration library. For developers, they don’t want to have code for annotation handlers in their compiled projects. Therefore, it is necessary that the annotation declaration library and annotation handler library are not packaged into the project! In other words, the annotation handler is only needed during the compilation process, after which it is useless, and adding the library to the main project introduces a lot of unnecessary files.

Since I'm an Android developer myself, the following is a discussion of the Android project.

Use the android – apt

Anroid-apt is a Gradle plugin developed by Hugo Visser. The main functions of this plugin are as follows:

  • Allows only compile-time annotation handlers to be configured as dependencies and not to include artifacts in the final APK or library
  • Set the source path so that Android Studio correctly finds the code generated by the comment handler

Google’s Android Gradle plugin has been added with annotationProcessor to completely replace Android – APT. Let’s take a look at annotationProcessor in action.

AnnotationProcessor use

In fact, the use of annotationProcessor is also very simple, which can be divided into two types, as shown in the following code:

 annotationProcessor project(':apt_compiler')// If it is a local library annotationProcessor'com. Jakewharton: butterknife - compiler: 9.0.0 - rc1'// For remote librariesCopy the code

conclusion

In the whole APT process, I have consulted a lot of information and solved many problems. Although blogging also takes a lot of time. But I also found a lot of interesting questions. I found a common problem with all the relevant materials I looked up. That is, I don’t really understand the specific functions of android apt and annotationProcessor. So here is also to warn you, for the information on the Internet, you must take a skeptical and questioning attitude to browse.

At the same time, I think the knowledge of Gradle is very important. Because how not to package libraries into real projects is also a feature and function of the build tool. Hope you have time, must learn about Gradle knowledge. The author has been studying recently. Come on with me

I have submitted the code in this article to GitHub. Download the source code on demand from —->

The last

This article references the following blogs and books, standing on the shoulders of giants. You can see further.

ANNOTATION PROCESSING 101

Compile-time annotations for custom annotations (retentionPolicy.class)

You must know APT, annotationProcessor, Android-apt, Provided, custom annotations