Android APT annotation literacy

preface

Little apes who have just come into contact with java may not know what annotation is for, or some old birds know only a little and can only use it without knowing why. Therefore, before understanding what annotations are, let me first talk about the use of God horses, the scenes, and whether there are templates at present. After knowing this, let's see if you need to learn at the current stage or use it later. First, let's talk about the usefulness. Annotations can mainly simplify the repetitive work and automation process of developers, but do not improve the efficiency of code execution. They are used for the construction of frameworks and the development of tools; Most application scenarios include automatic code generation in the compilation stage and runtime stage; At present, there are many frameworks developed with annotations, such as ButterKnife, EventBus, Retrofit,ARouter, etc. Well, I've been gossiping so much. Now let's talk about what you can know after reading this article. Let's go to the directory first

  • Concept of annotation
  • Meta annotation
  • Custom annotations (use of annotations)
    • Runtime annotation (engine -- > reflection)
    • Compile time annotation (core -- > annotation parser)
  • Automatic code generation help library javapool

What is annotation

Annotations are special tags in code. These tags can be read during compilation, class loading and runtime, and perform corresponding processing. By using Annotation, developers can embed some supplementary information in the source file without changing the original logic. Code analysis tools, development tools and deployment tools can be verified, processed or deployed through these supplementary information

Meta annotation

Meta annotation is the annotation of annotation. Cry and say what. In fact, it can be understood as an analogy to the keywords provided by java. At present, meta annotations are divided into four types as follows (analogy to multiple keywords provided by java). How each meta annotation (keyword) works is the matter of the compiler.

1.@Retention: defines the life cycle of the annotation (to what extent the described annotation is valid). The parameter is the RetentionPolicy enumeration object

@Retention retention policy typeexplain
RetentionPolicy.SOURCEAnnotations are only kept in the source file. When Java files are compiled into class files, annotations are discarded
RetentionPolicy.CLASSAnnotations are retained in the class file, but are discarded when the jvm loads the class file. This is the default life cycle for compile time annotations
RetentionPolicy.RUNTIMEAnnotations are not only saved to the class file, but also exist after the jvm loads the class file (for runtime annotations)

2.@Target: indicates where our annotation can appear (not including all), and the parameter is an array of ElementType type

@Target ElementType typeexplain
ElementType.TYPEInterface, class, enumeration, annotation
ElementType.FIELDField, enumerated constant
ElementType.METHODmethod
ElementType.PARAMETERMethod parameters
ElementType.CONSTRUCTORConstructor
ElementType.LOCAL_VARIABLElocal variable
ElementType.ANNOTATION_TYPEannotation
ElementType.PACKAGEpackage

3. @ document (less used)

It indicates that the annotations we marked can be documented by tools such as javadoc

4. @ inherited (less used)

Indicates that the annotation we marked is Inherited, and the default is false. For example, if a parent class uses an annotation decorated with @ Inherited, the child class is allowed to inherit the annotation of the parent class.

Custom annotation

Customize the annotation process, so easy as follows:

  1. Declaration note
  2. Parsing annotations (two modes are as follows)
    • Reflective annotation processor (runtime annotation)
    • Implementation class of AbstractProcessor (compile time annotation)
  3. Configure annotation parser
  4. Use of annotations

Let's demonstrate these steps through AS. First, create Module1: Annotation (select Java library), which is used to declare annotations. For example: (ARouter is used to simulate Ali's routing framework, Bindview is used to simulate Butterknife Library)

/**
 * View view comments by ID
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FindViewById {
    // If value is used for naming, it can be ignored during use, otherwise the parameter name must be added
    int value();
}
/**
 * Set click event
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SetOnClickListener {
    int id();
    String methodName();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ARouter {
    String path();
    String group() default "";
}

Step 2: create Module2: compiler (select Java Library or Android Library), which is used to interpret annotations

Note: generally, Android Library is selected for runtime annotation, and Java Library is used for compilation annotation (because AbstractProcessor class depends on rt.jar package)

Reflective annotation processor (runtime annotation)

import android.app.Activity;
import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * Annotation Parser
 */
public class ButterKnifeProcess{
    /**
     * analysis
     * @param target Resolution target
     */
    public static void parse(final Activity target) {
        try {
            Class<?> clazz = target.getClass();
            // Get all fields
            Field[] fields = clazz.getDeclaredFields(); 
            FindViewById byId;
            SetOnClickListener clkListener;
            View view;
            String name;
            String methodName;
            int id;
            for (Field field : fields){
                //Returns all annotations that exist on the element
                Annotation[] annotations = field.getAnnotations();
                for(Annotation annotation:annotations) {
                    if (annotation instanceof FindViewById) {
                        // Gets the FindViewById object (returns the annotation of the specified type existing on the element)
                        byId = field.getAnnotation(FindViewById.class);
                        // This operation is required for reflection access to private members
                        field.setAccessible(true); 
                        //Field name
                        name = field.getName(); 
                        id = byId.value();
                        // Find objects
                        view = target.findViewById(id);
                        if (view != null)
                            field.set(target, view);
                        else
                            throw new Exception("Cannot find.View name is " + name + ".");
                    } else if (annotation instanceof SetOnClickListener) { // Set click method
                        //Returns an annotation of the specified type that exists on the element
                        clkListener = field.getAnnotation(SetOnClickListener.class);
                        field.setAccessible(true);
                        // Get variable
                        id = clkListener.id();
                        methodName = clkListener.methodName();
                        name = field.getName();
                        view = (View) field.get(target);
                        if (view == null) { // If the object is empty, find the object again
                            view = target.findViewById(id);
                            if (view != null)
                                field.set(target, view);
                            else
                                throw new Exception("Cannot find.View name is " + name + ".");
                        }
                        // Returns all methods of the current class, including public, private and protected, excluding the parent class
                        Method[] methods = clazz.getDeclaredMethods();
                        boolean isFind = false;
                        for (final Method method:methods) {
                            if (method.getName().equals(methodName)) {
                                isFind = true;
                                view.setOnClickListener(new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        try {
                                            method.invoke(target);
                                        } catch (Exception e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });

                                break;
                            }
                        }

                        // Can't find
                        if (!isFind) {
                            throw new Exception("Cannot find.Method name is " + methodName + ".");
                        }

                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Implementation class of AbstractProcessor (compile time annotation)

package com.jsonan.compile;

import com.google.auto.service.AutoService;
import com.jsonan.annotation.ARouter;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
 * AbstractProcessor Is an abstract class that implements the Processor interface.
 * Processor The implementation class of the interface must provide a public parameterless constructor, which will be used by the virtual machine tool to instantiate the Processor.
 * The interaction process between the virtual machine tool framework and the class implementing this interface:
 * 1.Create Processor object 
 * 2.Call the init method of the Processor object
 * 3.Call the getSupportedAnnotationTypes, getSupportedOptions and getSupportedSourceVersion methods of the Processor object (we will implement these methods using annotations below)
 * 4.Calling the process method of the Processor object
 */
@AutoService(Processor.class)
// Sets the name of the annotation type supported by this annotation parser
@SupportedAnnotationTypes("com.jsonan.annotation.ARouter")
// Set the latest JDK version supported by this annotation parser
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Set the options recognized by this annotation parser
@SupportedOptions("content")
public class ARouterProcessor extends AbstractProcessor {
	//A tool class used to handle Element elements
    private Elements elementUtils;
    //Tool class used to handle TypeMirror
    private Types typeUtils;
    //Output warning, error log tool class
    private Messager messager;
    //File generator
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnvironment.getElementUtils();
        typeUtils = processingEnvironment.getTypeUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        //The parameters of the content are from build. In the annotated Module Transfer in gradle, such as:
        //android {
		//    defaultConfig {
        //        javaCompileOptions{
        //            annotationProcessorOptions {
        //                arguments = [content:'hello apt']
        //            }
        //        }
		//    }
        String content = processingEnvironment.getOptions().get("content");
        messager.printMessage(Diagnostic.Kind.NOTE, content);
    }
	/**
     *The results scanned by the annotation Processor will be stored in the roundEnvironment, where you can get the annotation content and write your operation logic
     *Note: you cannot throw an exception directly in the process() function, otherwise the program will crash abnormally
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) {
            return false;
        }
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        for (Element element : elements) {
            String packName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            String className = element.getSimpleName().toString();
            messager.printMessage(Diagnostic.Kind.NOTE, "The annotated classes are:" + className);
            String finalClassName = className + "$$ARouter";
            try {
                				                             //This will be displayed in app \ build \ generated \ AP_ generated_ Generate mainactivity $$arouter under sources \ debug \ XXX \ package Java file
                JavaFileObject sourceFile = filer.createSourceFile(packName + "." + finalClassName);
                Writer writer = sourceFile.openWriter();
                writer.write("package " + packName + ";\n");
                writer.write("public class " + finalClassName + "{\n");
                writer.write("public static Class<?> findTargetClass(String path){\n");
                // Returns an annotation of the specified type that exists on this element
                ARouter aRouter = element.getAnnotation(ARouter.class);
                writer.write("if (path.equalsIgnoreCase(\"" + aRouter.path() + "\")){\n");
                writer.write("return " + className + ".class;\n}\n");
                writer.write("return null;\n");
                writer.write("}\n}\n");
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        return false;
    }
}

The core of the annotation processor is the process() method, and the core of the process() method is the element element. Element represents the element of the program. During annotation processing, the compiler will scan all Java source files and treat each part of the source code as a specific type of element. It can represent packages, classes, interfaces, methods, fields and other element types. All elements must have several subclasses. As shown below.

Element subclassexplain
TypeElementClass or interface element
VariableElementField, enum constant, method or constructor parameter, local variable or exception parameter element
ExecutableElementA method, constructor, or annotation type element of a class or interface
PackageElementPackage element
TypeParameterElementGeneric parameters of a class, interface, method, or constructor element

We also give a brief introduction to the methods in the Element class:

    /**
     * Returns the types defined by this element, int,long
     */
    TypeMirror asType();

    /**
     * Return the type of this element: package, class, interface, method and field
     */
    ElementKind getKind();

    /**
     * Returns the modifiers of this element: public, private, protected
     */
    Set<Modifier> getModifiers();

    /**
     * Returns the simple name (class name) of this element
     */
    Name getSimpleName();

    /**
     * Returns the innermost element that encapsulates this element.
     * If the declaration of this element is lexically directly encapsulated in the declaration of another element, the encapsulated element is returned;
     * If this element is a top-level type, its package is returned;
     * If this element is a package, null is returned;
     * null if this element is a generic parameter
     */
    Element getEnclosingElement();

    /**
     * Returns the child element directly encapsulated by this element
     */
    List<? extends Element> getEnclosedElements();

    /**
     * Returns the annotation that exists directly on this element
     * To get inherited annotations, use getAllAnnotationMirrors
     */
    List<? extends AnnotationMirror> getAnnotationMirrors();

    /**
     * Returns an annotation of the specified type that exists on this element
     */
    <A extends Annotation> A getAnnotation(Class<A> var1);

Use examples of subclasses of Element are as follows:

//VariableElement modifies the annotation of attributes and class members
for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {
    //ElementType.FIELD annotation can be directly forced to VariableElement
    VariableElement variableElement = (VariableElement) element;
	//The getEnclosingElement attribute, the direct encapsulation of class members, and the upper element is the class, so it is a TypeElement
    TypeElement classElement = (TypeElement) element.getEnclosingElement();
    PackageElement packageElement = elementUtils.getPackageOf(classElement);
    //Class name
    String className = classElement.getSimpleName().toString();
    //Package name
    String packageName = packageElement.getQualifiedName().toString();
    //Class member name
    String variableName = variableElement.getSimpleName().toString();

    //Class member type
    TypeMirror typeMirror = variableElement.asType();
    String type = typeMirror.toString();
}

Note: in the process() function above, we use the JavaFileObject method to generate java files. If there are many java files and the content is complex, it is easy to make mistakes. There is a convenient tool library for us to use javapool. The last part of the following will be described in detail. Skip for the moment and finish the whole process first

Step 3:

Method 1: create javax annotation. processing. Processor file: under the main folder, create the resources folder in the same level directory as java, and then create meta-inf / services / javax annotation. processing. Processor file, in javax annotation. processing. The processor file indicates the class path of the annotation parser class processor (the full class name of the processor). If there are multiple annotation processors, it can be written on a new line.

Mode 2:

  1. In the build. Of processor Module: compile Add google's auto generation service library to gradle, such as:

    dependencies {
        implementation 'com.google.auto.service:auto-service:1.0-rc4'
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
        implementation project(path:':annotation')
    }
    sourceCompatibility = "7"
    targetCompatibility = "7"
    
  2. Add @ AutoService(Processor.class) before the annotation processor class, such as:

    @AutoService(Processor.class)
    public class ButterKnifeProcess{
    	...
    }
    

Method 3: if the Module created in step 2 is Android Library, you can create it in build In the dependencies block of the gradle file, add the support annotations dependency

dependencies { compile 'com.android.support:support-annotations:24.2.0' } 

If the module uses the appcompat library, there is no need to add a support annotations dependency. Because the appcompat library already depends on the annotation library

Step 4: to use the annotation, you need to use it in the Module build Add dependencies to the gradle file. For example, if you want to use the main Module: app in the project, you can use it in the build. App of the main Module Add dependencies to gradle:

dependencies {
    annotationProcessor project(path: ':compile(Parsing annotation Module Name of)')
    implementation project(path: ':annotation(Declaration note Module Name of)')
}

If the annotation resolution Module written is only scanned at compile time, you can also modify the dependency to:

dependencies {
    annotationProcessor project(path: ':compile(Parsing annotation Module Name of)')
    complileOnly project(path: ':annotation(Declaration note Module Name of)')
}

Compile time annotation and run-time annotation are used as follows:

//Runtime annotation
@ARouter(path = "/app/MainActivity", group = "app")
public class MainActivity extends AppCompatActivity {
	
    //Automatically bind view (compile time annotation)
    @FindViewById(R.id.tv_Toast)
    @SetOnClickListener(id = R.id.tv_Toast, methodName = "click")
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnifeProcess.parse(this);
    }
    /**
     * Click event
     */
    public void click() {
        Toast.makeText(this, "Hello compile annotatioin", Toast.LENGTH_SHORT).show();
    }
}

A brief introduction and use of javapool

Javapool is a third-party library that automatically generates java files by programming. It is usually used to write templates, classes, frameworks, etc

Use it directly (benchmark the MainActivity$$ARouter.java file generated when compiling in Filer mode above). Refer to the following for the specific introduction of javapool Reference link

Files that need to be generated to use javapool

package com.jsonan.annotationlearning;

import java.lang.Class;
import java.lang.String;

public class MainActivity$$ARouter {
    public static Class<?> findTargetClass(String path) {
        if (path.equalsIgnoreCase("/app/MainActivity")) {
            return MainActivity.class;
        }
        return null;
    }
}

Javapool uses the following:

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    ClassName path = ClassName.get("java.lang", "String");
    ParameterSpec savedInstanceState = ParameterSpec.builder(path, "path").build();
    //Construct a method
    MethodSpec findTargetClass = MethodSpec.methodBuilder("findTargetClass")      //name
               .addModifiers(Modifier.PUBLIC, Modifier.STATIC)         //modification
               .returns(Class.class)                                   //return
               .addParameter(savedInstanceState)                       //parameter
        .beginControlFlow("if($N.equalsIgnoreCase(\"/app/MainActivity\"))",savedInstanceState)
               .addStatement("return MainActivity.class")  //sentence
               .endControlFlow()
               .addStatement("return null")  //sentence
               .build();
    //Construct a class
    TypeSpec MainActivity$$ARouter = TypeSpec.classBuilder("MainActivity$$ARouter")         		  //name
             .addModifiers(Modifier.PUBLIC)                         //modification
             .addMethod(findTargetClass)                            //method
                        .build();
    JavaFile javaFile = JavaFile.builder("com.jsonan.annotationlearning",MainActivity$$ARouter).build();
        try {
                javaFile.writeTo(System.out);
            } catch (IOException e) {
                e.printStackTrace();
            }
        File file = new File("."+"\\src\\");
        if (file.exists()) {
            file.delete();
        }
        javaFile.writeTo(file);
        return false;
    }

Reference link

https://blog.csdn.net/wuyuxing24/article/details/81139846

https://blog.csdn.net/wangjiang_qianmo/article/details/99350360

Introduction to Element subclass

Introduction and use of javapool

javapoet official website

Keywords: Android

Added by twopeak on Mon, 17 Jan 2022 11:22:21 +0200