The nature of annotations

Annotations are often used in programming, such as Override, find the Annotation interface, and then Control + H can find that Override inherits from the Annotation.

The Annotation of the document

So the essence of an Annotation is simple: an Annotation is an interface that inherits an Annotation.

The role of annotations

Annotations are used to mark the Class,Field, and Method in SourceCode to add extra information to a piece of code, which can be interpreted as a programmer’s way of being lazy. For example, findViewById is cumbersome to write, so use a butterknife to steal the day:

 @BindView(R.id.user) EditText username;
Copy the code

So since the tag needs to be parsed behind, otherwise can only be viewed as a comment, the specific way of parsing will be discussed later.

Define a custom annotation

Define an annotation called Name that has a method value that sets a String value:

@Target(value = {ElementType.FIELD, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Name {
    String value(a);
}
Copy the code

We’ll see that annotations also have annotations on them, like @target,@Retention above, and these annotations that describe annotations are called meta-annotations.

Yuan notes

  • @target: Describes the Target of the annotation, such as a field or a method, such as the following common types:

    • ElementType.TYPE: applies to classes and interfaces
    • ElementType.FIELD: applies to fields
    • ElementType.METHOD: applies to methods
  • @Retention: The life cycle of annotations:

    • RetentionPolicy.SOURCE: visible at compile time, not written to the class file,Usually used to automatically generate code when APT compiles
    • RetentionPolicy.CLASS: Discarded during class loading and written to the class file, invisible at runtime
    • RetentionPolicy.RUNTIME: is saved permanently and can be retrieved by reflection at runtime

The other meta-annotations @Inherited, @Documented are very simple and won’t be discussed here.

Parse custom annotations

According to the Annotation life cycle, parsing methods can be roughly divided into two categories. For example, for RUNTIME identification, parsing can be done through reflection at RUNTIME, and for SOURCE identification, parsing can be done through APT (Annotation Processing Tool) at compile time.

Runtime reflection parses annotations

  1. Get an annotated class, method, or field by reflection
  2. Gets annotation information on the annotation object
public class Test {
    public static void main(String[] args){
        testAnnotation();
    }
    @Name(value = "Mr.test")
    public static void testAnnotation(a){
        try {
            Class clazz = Test.class;
            // 1. Get the annotated method
            Method method = clazz.getMethod("testAnnotation".null);
            // 2. Get the annotation on method and its value
            Name name = method.getAnnotation(Name.class);
            System.out.println(name.value());
        } catch(NoSuchMethodException e) { e.printStackTrace(); }}}Copy the code

Annotations are parsed at compile time

APT is a tool that javac uses to handle annotations. It can scan all annotations at compile time, get the annotation value and the annotated Element, and then customize some operations. For example, it can check whether a class has a parameterless constructor. You can also generate some template code from annotations at compile time, as Butterknife does, for example. The steps for parsing annotations at compile time are as follows:

  • Custom annotation classes
  • Implement a subclass of AbstractProcessor called BindViewProcessor
  • Let the compiler recognize the custom BindViewProcessor
Custom annotation classes

Create a new JavaModule called annotation-lib. Define a BindView class that applies to a Field and only exists at compile time. It has a value method that gets the id of the annotated View:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    int value(a);
}
Copy the code
Define a subclass of AbstractProcessor

Create a new JavaModule called annotation-processor that relies on annotation-lib in build.gradle:

dependencies {
    implementation project(':annotation-lib')
}
Copy the code

Custom BindViewProcessor is as follows:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtil;
    private Map<String, ClassCreatorProxy> proxyMap = new HashMap<>();
    private ProcessingEnvironment processingEnv;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        System.out.println("-------BindViewProcessor init");
        elementUtil = processingEnvironment.getElementUtils();
        processingEnv = processingEnvironment;
    }
    /** * Specifies the Java version, which can be annotated@SupportedSourceVersion(SourceVersion.RELEASE_8) instead of */
    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        return SourceVersion.latestSupported();
    }
    /** * set what type of annotations this handler handles, which can be annotated@SupportedAnnotationTypes(com. Wangzhen. Annotation_complier. StudentProcessor) instead of * /
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        HashSet<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }
    / * * *@paramSet request processing annotation type *@param roundEnvironment
     * @returnTrue: indicates that the current annotation has been processed. False: Subsequent processors may be required to process */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("-------BindViewProcessor process");
        // Todo parses annotations to generate Java files
        return true; }}Copy the code

The four methods that need to be implemented have comments in their code that I won’t go into here, but the init method has a ProcessingEnvironment parameter and the Process method has a RoundEnvironment parameter. What are these parameters for?

ProcessingEnvironment
package javax.annotation.processing;
public interface ProcessingEnvironment {
    // Returns a Messager to report errors, alerts, and other notifications, which can be used to print logs.
    Messager getMessager(a);
    // Returns the Filer used to create a new source, class, or auxiliary file.
    Filer getFiler(a);
    // Returns the utility method used to operate on Element.
    Elements getElementUtils(a);
    // Utility methods used to manipulate types.
    Types getTypeUtils(a); . }Copy the code

GetFiler returns a Filer that supports creating a new file:

public interface Filer {
// Create a new source file and return an object to allow writing to it.
    JavaFileObject createSourceFile(CharSequence var1, Element... var2) throws IOException;
// Create a new class file and return an object to allow writing to it.

    JavaFileObject createClassFile(CharSequence var1, Element... var2) throws IOException;
// Create a new secondary resource file for the write operation and return a file object for it.
    FileObject createResource(Location var1, CharSequence var2, CharSequence var3, Element... var4) throws IOException;
// Returns an object to read from an existing resource.
    FileObject getResource(Location var1, CharSequence var2, CharSequence var3) throws IOException;
}

Copy the code

For example, we will use its createSourceFile to create a Java file.

Elements operate on Element. What is Element? Element represents a program Element, such as a package, class, or method. It has several common subclasses:

Element | - PackageElement / / package elements, Can get the package name | - TypeElement / / a class or interface | - VariableElement / / said a field, enum constants, the method or constructor parameters, local variables, or abnormal.Copy the code

We can see that Element actually contains some information about the code we are writing. It is commonly used as follows:

package javax.lang.model.element;
public interface Element extends AnnotatedConstruct {
    // Returns an element directly encapsulated (not strictly) by this element, such as its class if it is a VariableElement.
    List<? extends Element> getEnclosedElements();
   
    // Get the annotation on this element
    <A extends Annotation> A getAnnotation(Class<A> var1); . }Copy the code

The common methods of Elements are as follows:

package javax.lang.model.util;

public interface Elements {
    // Returns the package element of the given element
    PackageElement getPackageOf(Element var1);
    // Returns all members of a type element, whether inherited or declared directly.List<? extends Element> getAllMembers(TypeElement var1); . }Copy the code

So we can use Elements to get all the package Elements, and we can get the package name of the new Java file. We can create a Java file using Filer. Now what’s missing is to get the Element annotated by BindView. It is available through RoundEnvironment.

RoundEnvironment

Common methods of RoundEnvironment are as follows:

package javax.annotation.processing;

public interface RoundEnvironment {
   // Returns an element with an annotation of the given type.
    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
    // Returns an element with an annotation of the given type.Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1); . }Copy the code
Spell the string in the process method and generate the file
@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("-------BindViewProcessor process");
        proxyMap.clear();
        // 1. Get all elements annotated by BindView. Since BindView applies to Field, these elements must be fields
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        // 2. Loop through all annotated elements
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            // 3. Get the class it belongs to
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy classCreatorProxy = proxyMap.get(fullClassName);
            if (classCreatorProxy == null) {
                classCreatorProxy = new ClassCreatorProxy(elementUtil, classElement);
                proxyMap.put(fullClassName, classCreatorProxy);
            }
            // 4. Get the information in its annotations
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            classCreatorProxy.putElement(id, variableElement);
            // create a Java file
            createJavaFile();
        }
        return true;
    }

    private void createJavaFile(a){
        for(String key:proxyMap.keySet()){
            ClassCreatorProxy proxyInfo = proxyMap.get(key);
            try {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                // 6. Spell the code string in the Java file
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
                System.out.println("-------BindViewProcessor create java file success");
            } catch (IOException e) {
                System.out.println("-------BindViewProcessor create java file failed"); e.printStackTrace(); }}}Copy the code

ClassCreatorProxy is simply used to spell code strings:

package com.wangzhen.annotation_processor;

public class ClassCreatorProxy {
    private String bindingClassName;
    private String packageName;
    private TypeElement typeElement;
    private Map<Integer, VariableElement> variableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elements, TypeElement classElement) {
        this.typeElement = classElement;
        PackageElement packageElement = elements.getPackageOf(typeElement);
        packageName = packageElement.getQualifiedName().toString();
        bindingClassName = typeElement.getSimpleName() + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element) {
        variableElementMap.put(id, element);
    }

    public String generateJavaCode(a) {
        StringBuilder builder = new StringBuilder();
        // Spell the package name
        builder.append("package ").append(packageName).append("; \n\n")
        // Spelling dependency
            .append("import com.wangzhen.annotation_lib.*;").append("\n\n")
            // Spelling class structure
            .append("public class ").append(bindingClassName).append(" {\n");
        generateMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }
    // Spelling
    private void generateMethods(StringBuilder builder) {
        builder.append("public void bind(" + typeElement.getQualifiedName() + " host ) {\n");
        for (int id : variableElementMap.keySet()) {
            VariableElement element = variableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append("host." + name).append("=");
            builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + ")); \n");
        }
        builder.append(" }\n");
    }

    public String getProxyClassFullName(a) {
        return packageName + "." + bindingClassName;
    }

    public TypeElement getTypeElement(a) {
        returntypeElement; }}Copy the code
Let the compiler recognize the custom BindViewProcessor

Although the code is written, we still need the compiler to execute our code during compilation, which consists of two main steps:

  • The easiest way to declare our BindViewProcessor to the compiler is to use the AutoService provided by Google:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {... }Copy the code

Using AutoService requires importing dependencies under annotaiton-Processor Module:

dependencies {
    implementation fileTree(dir: 'libs'.include: ['*.jar'])
    implementation 'com. Google. Auto. Services: auto - service: 1.0 - rc6'
    implementation project(':annotation-lib')}Copy the code
  • Add dependency annotation-Processor to main Module app
dependencies{... implementationproject(':annotation-lib')
    AnnotationProcessor is required
    annotationProcessor project(':annotation-processor')}Copy the code

use

Use BindView for MainActivity in app, and then use reflection to call mainactivity_viewbinding. Java we generated:

public class MainActivity extends AppCompatActivity {
    @BindView(value = R.id.tv_test)
    TextView tvTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bind();
        tvTest.setText("bind success");
    }

    private void bind(a) {
        try {
            Class bindViewClazz = Class.forName(this.getClass().getName() + "_ViewBinding");
            Method method = bindViewClazz.getMethod("bind".this.getClass());
            method.invoke(bindViewClazz.newInstance(), this);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch(InstantiationException e) { e.printStackTrace(); }}}Copy the code

Now let’s compile the project and the compiler will generate the Java file we spelled in the following path:

app
|- build
|-- generated
|--- source| - apt | -- -- -- -- -- the debug | -- -- -- -- -- - your package name | -- -- -- -- -- -- -- MainActivity_ViewBinding. JavaCopy the code

MainActivity_ViewBinding. Java contains the following contents:

package com.wangzhen.annotationjavatest;

import com.wangzhen.annotation_lib.*;

public class MainActivity_ViewBinding {
    public void bind(com.wangzhen.annotationjavatest.MainActivity host) {
        host.tvTest = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165359)); }}Copy the code

Generate code from Javapoet

Spelling strings manually is error-prone and cumbersome. You can use Javapoet to write Java source code like apoet:

public TypeSpec generateJavaCodeByPoet(a){

        TypeSpec bindingClass = TypeSpec.classBuilder(bindingClassName)
            .addModifiers(Modifier.PUBLIC)
            .addMethod(generateMethodByPoet())
            .build();
        return bindingClass;
    }

    private MethodSpec generateMethodByPoet(a){
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
            .addModifiers(Modifier.PUBLIC)
            .returns(void.class)
            .addParameter(ClassName.bestGuess(typeElement.getQualifiedName().toString()),"host");

        for (int id : variableElementMap.keySet()) {
            VariableElement element = variableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            String codeString = "host." + name + "=" + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));";
            methodBuilder.addCode(codeString);
        }
        return methodBuilder.build();
    }
Copy the code

code

github

reference

【Android】APT (generate code at compile time)