Write at the beginning: While reading some open source libraries recently, I found that most of them use annotations, so I had to take a closer look at the knowledge of annotations under Android

What are annotations

Java.lang. annotation, the interface annotation, was introduced in JDK5.0 and later.

Annotations are special tags in your code that can be read at compile time, class load time, run time, and perform processing accordingly. Using annotations, developers can embed additional information in source files without changing the original logic. Code analysis tools, development tools, and deployment tools can use this supplementary information to validate, process, or deploy.

The Annotation doesn’t run, it just has member variables, it has no methods. Annotations are part of a program element in the same way that modifiers such as public and final are. Annotations cannot be used as a program element.

The role of annotations

Annotations make repetitive tasks automatic, simplifying and automating the process. For example, it is used to generate Java Doc, check the format during compilation, and automatically generate code to improve the quality of software and improve the production efficiency of software.

Common notes

There are roughly four types of annotations that Android has defined, called meta-annotations

@Retention: Defines how long the Annotation is retained

  • RetentionPoicy.SOURCEAnnotations are only retained in the source file. When a Java file is compiled into a class file, annotations are discarded. It is used to perform some inspection operations, such as@Override@SuppressWarnings
  • RetentionPoicy.CLASS:Annotations are kept in the class file, but are discarded when the JVM loads the class file, which is the default lifecycle; Used for pre-processing operations at compile time, such as generating auxiliary code (e.gButterKnife)
  • RetentionPoicy.RUNTIME:Annotations are not only saved to the class file, but still exist after the JVM loads the class file; Used to dynamically retrieve annotation information at run time. This annotation will be used with reflection

@target: Defines the range of objects that the Annotation decorates

  • ElementType.CONSTRUCTOR: describes the constructor
  • ElementType.FIELD: Describes a domain
  • ElementType.LOCAL_VARIABLE: Describes local variables
  • ElementType.METHOD: Describes a method
  • ElementType.PACKAGE: Describes packages
  • ElementType.PARAMETER: Describes parameters
  • ElementType.TYPE: Describes classes, interfaces (including annotation types), or enum declarations

Not marked means can be modified all

@Inherited: Whether a child is allowed to inherit annotations from its parent. The default value is false

Documented Whether @documented will be saved to a Javadoc document

Custom annotations

Run-time annotations and compile-time annotations are the most commonly used custom annotations

Runtime annotations

This is illustrated with a simple example of dynamically bound controls

Start by defining a simple custom annotation,

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value() default  -1;
}
Copy the code

We then use reflection to inject the controls from findViewbyId() into the variables we need when the app runs.

public class AnnotationActivity extends AppCompatActivity { @BindView(R.id.annotation_tv) private TextView mTv; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_annotation); getAllAnnotationView(); mTv.setText("Annotation"); } private void getAllAnnotationView() {Field[] fields = this.getClass().getDeclaredFields(); For (Field Field: fields) {try {// Annotations if (field.getannotations ()! = null) {/ / sure annotation types if (field. IsAnnotationPresent (BindView. Class)) {/ / allows field. The modified reflection properties setAccessible (true); BindView bindView = field.getAnnotation(BindView.class); Field. Set (this, findViewById(bindView.value()))); } } } catch (Exception e) { e.printStackTrace(); }}}}Copy the code

And then finally the mTv shows the Annotation text that we want, which looks a little bit like a ButterKnife, but remember that reflection is a performance drain,

So instead of using runtime annotations, our common control-binding library, ButterKnife, uses compile-time annotations.

Compile-time annotations

define

Before we talk about compile-time annotations, let’s mention AbstractProcessor, an annotation processor. It’s a javac tool that scans and handles annotations at compile time. You can customize annotations and register them with the Annotation handler that handles your annotations.

An annotation processor that takes Java code (or compiled bytecode) as input and generates a file (usually a.java file) as output. This.Java code generated by the annotator can be compiled by Javac just like regular.Java.

The import

AbstractProcessor can’t be called directly in Android projects because it is a javac tool. The following provides a viable import method that I try.

File–>New Module–> Java Library Create a New Java Module. Make sure it’s Java Library, not Android Library

You can then use AbstractProcessor in the corresponding library

With all the preparation done, let’s take a look at a simple annotation bound control example

Project directory

-- APP (Main project) -- App_ANNOTATION (Java Module custom annotations) -- Annotation-API (Android Module) -- APP_compiler (Java Module annotations processor logic)Copy the code

Create an annotation under the Annotation Module

@retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) }Copy the code

Create the annotation processor CustomProcessor under compiler Module

Public class CustomProcessor extends AbstractProcessor {private Filer mFiler; // Private Elements mElements; @override public synchronized void init(ProcessingEnvironment ProcessingEnvironment) { super.init(processingEnvironment); mElements = processingEnvironment.getElementUtils(); mFiler = processingEnvironment.getFiler(); Override public Boolean process(Set<?) @override public Boolean process(Set<?  extends TypeElement> set, RoundEnvironment env) { return true; } / / custom annotation Set @ Override public Set < String > getSupportedAnnotationTypes () {Set < String > types = new LinkedHashSet < > (); types.add(BindView.class.getCanonicalName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }}Copy the code

The core process function takes two arguments, the second of which we focus on because env represents the set of all annotations

First of all, we will briefly explain the processing process of Porcess

  1. Iterate over env to get a list of the elements we need
  2. Encapsulate the list of elements as objects for later processing (as you would normally parse JSON data)
  3. The JavaPoet library generates Java files from objects in the form we expect
  1. Iterate over env to get a list of the elements we need
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){ // todo .... Class if (element.getkind () == elementkind.class) {TypeElement TypeElement = (TypeElement); element; / / output element names System. Out. Println (typeElement. GetSimpleName ()); / / output annotation attribute value System. Out. Println (typeElement. GetAnnotation (BindView. Class). The value ()); }}Copy the code

A list of required annotations can be obtained directly from the getElementsAnnotatedWith function, with some elements added inside the function for easy use

2. Encapsulate the list of elements as objects for later processing

First, we need to make it clear that under this event that binds the control, we need the id of the control.

Create a new class called bindViewField. class to hold the attributes associated with the custom annotation BindView

BindViewField.class

public class BindViewField {

    private VariableElement mFieldElement;

    private int mResId;

    public BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mFieldElement = (VariableElement) element;
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();
        if (mResId < 0) {
            throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
                    BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
        }
    }

    public Name getFieldName() {
        return mFieldElement.getSimpleName();
    }

    public int getResId() {
        return mResId;
    }

    public TypeMirror getFieldType() {
        return mFieldElement.asType();
    }
}
Copy the code

The above BindViewField can only represent one custom Annotation bindView object. A class can have multiple custom annotations, so we need to create an Annotation. Class object to manage the collection of custom annotations.

AnnotatedClass.class

Public class AnnotatedClass {// class public TypeElement mClassElement; Public List<BindViewField> mFiled; Public Elements mElementUtils; public AnnotatedClass(TypeElement classElement, Elements elementUtils) { this.mClassElement = classElement; this.mElementUtils = elementUtils; this.mFiled = new ArrayList<>(); Public void addField(BindViewField field) {mFiled. Add (field); } public String getPackageName(TypeElement Type) {return mElementUtils.getPackageOf(type).getQualifiedName().toString(); } private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; return type.getQualifiedName().toString().substring(packageLen).replace('.', '$'); }}Copy the code

Give the complete parsing process

Private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>(); @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { mAnnotatedClassMap.clear(); try { processBindView(roundEnvironment); } catch (Exception e) { e.printStackTrace(); return true; } return true; } private void processBindView(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field); System.out.print("p_element=" + element.getSimpleName() + ",p_set=" + element.getModifiers()); } } private AnnotatedClass getAnnotatedClass(Element element) { TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); String fullClassName = encloseElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(encloseElement, mElements); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; }Copy the code

3. Generate Java files from objects in the desired form through the JavaPoet library

FindViewById () is missing. ButterKnife’s popular library does not omit findViewById(). In the build/generated/source/apt/debug grown into a file, help to performed the findViewById () this behavior.

Similarly, we need to generate a Java file, using the JavaPoet library. Specific usage reference links

Add logic to generate Java files in the Process function

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    mAnnotatedClassMap.clear();
    try {
        processBindView(roundEnvironment);
    } catch (Exception e) {
        e.printStackTrace();
        return true;
    }

    try {
        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            annotatedClass.generateFinder().writeTo(mFiler);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
Copy the code

The core logic annotatedClass. GenerateFinder (.) writeTo (mFiler); Detailed implementation in AnnotatedClass

Public JavaFile generateFinder() {// Build inject method methodSpec.builder methodBuilder = methodspec.methodBuilder ("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(Utils.FINDER, "finder"); // host. Btn1 =(Button)finder.findView(source,2131427450); $N=($T)finder.findView(source,$L) ----  mFiled) { methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName() , ClassName.get(field.getFieldType()), field.getResId()); } String packageName = getPackageName(mClassElement); String className = getClassName(mClassElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); TypeSpec finderClass = TypeSpec. ClassBuilder (bindclassname.simplename () + "? Injector") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName. Get (mClassElement. AsType ()))) // Inheritance interface.addMethod(methodBuilder.build()).build(); return JavaFile.builder(packageName, finderClass).build(); }Copy the code

At this point, most of the logic has been implemented, and the helper classes used to bind the controls have been generated through JavaPoet, except for the last step, host registration, as ButterKnife, butterknife.bind (this)

Writing the call interface

New under annotation-API

Injector interface Injector

public interface Injector<T> {

    void inject(T host, Object source, Finder finder);
}
Copy the code

Host generic interface Finder(later extended to View and Fragment)

public interface Finder {

    Context getContext(Object source);

    View findView(Object source, int id);
}
Copy the code

The activity implements the ActivityFinder class

public class ActivityFinder implements Finder{ @Override public Context getContext(Object source) { return (Activity) source; } @Override public View findView(Object source, int id) { return ((Activity) (source)).findViewById(id); }}Copy the code

The core implementation class ButterKnife

public class ButterKnife {

    private static final ActivityFinder finder = new ActivityFinder();
    private static Map<String, Injector> FINDER_MAP = new HashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity);
    }

    private static void bind(Object host, Object source) {
        bind(host, source, finder);
    }

    private static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "?Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAP.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Copy the code

The main project was downgraded

The corresponding button can be used directly without requiring findViewById()

public class MainActivity extends AppCompatActivity { @BindView(R.id.annotation_tv) public TextView tv1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); tv1.setText("annotation_demo"); }}Copy the code

A brief introduction to JavaPoet

Several commonly used classes

  • MethodSpec represents a constructor or method declaration.
  • TypeSpec represents a class, interface, or enumeration declaration.
  • FieldSpec represents a member variable, a field declaration.
  • JavaFile contains a Java file for a top-level class.

A commonly used placeholder

$L for variable

$S for Strings

$T for Types

$N for Names(method or variable Names we generated ourselves, etc.)

Add the content

The main processing method in the annotation Processor is the process() function, and the important one in the process() function is the RoundEnvironment parameter,

Common usage

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
	//todo 
}
Copy the code

Get all Element objects through the BindView annotation, and what is that Element?

Element represents a program Element, which can be a package, class, or method. All elements retrieved through annotations are treated as Element types. To be precise, it is a subclass of the Element object.

Element of the subclass

  • ExecutableElementA method, constructor, or initializer (static or instance) that represents a class or interface, including annotation type elements.

    Corresponding to the annotation@Target(ElementType.METHOD)and@Target(ElementType.CONSTRUCTOR)
  • PackageElementRepresents a package element that provides access to information about a package and its members. Corresponding annotation time@Target(ElementType.PACKAGE)
  • TypeElementRepresents a class or interface program element that provides access to information about a type and its members. The corresponding@Target(ElementType.TYPE)

    Note: An enumeration type is a class, while an annotation type is an interface.
  • TypeParameterElementA type parameter representing a generic class, interface, method, or constructor element.

    The corresponding@Target(ElementType.PARAMETER)
  • VariableElementRepresents a field, enum constant, method or constructor parameter, local variable, or exception parameter.

    The corresponding@Target(ElementType.LOCAL_VARIABLE)

Different types of elements acquire information in different ways

Modify method annotations and executableElements

When you have an annotation defined as @target (elementType.method), it means that the annotation can only modify methods.

Get some basic information we need

// bindclick. class with @target (ElementType.METHOD) decorates for (Element Element: RoundEnv. GetElementsAnnotatedWith (BindClick. Class)) {/ / Element for direct strong ExecutableElement ExecutableElement = (ExecutableElement) element; / / the corresponding Element, by obtaining TypeElement getEnclosingElement conversion classElement = (TypeElement) Element. GetEnclosingElement (); / / when (ExecutableElement) element was established, using (PackageElement) element. GetEnclosingElement (); Will be an error. / / need to use elementUtils to obtain Elements elementUtils = processingEnv. GetElementUtils (); PackageElement packageElement = elementUtils.getPackageOf(classElement); . / / the class name String fullClassName = classElement getQualifiedName (), toString (); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / the method name String methodName = executableElement getSimpleName (), toString (); List<? extends VariableElement> methodParameters = executableElement.getParameters(); // List<String> types = new ArrayList<>(); for (VariableElement variableElement : methodParameters) { TypeMirror methodParameterType = variableElement.asType(); if (methodParameterType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) methodParameterType; methodParameterType = typeVariable.getUpperBound(); } / / parameter name String parameterName. = variableElement getSimpleName (). The toString (); / / parameter type String parameteKind = methodParameterType. ToString (); types.add(methodParameterType.toString()); }}Copy the code

Modify attributes, class members’ annotations, and VariableElements

for (Element element : RoundEnv. GetElementsAnnotatedWith (IdProperty. Class)) {/ / ElementType. Strong FIELD annotations can directly turn VariableElement VariableElement variableElement = (VariableElement) element; TypeElement classElement = (TypeElement) element .getEnclosingElement(); PackageElement packageElement = elementUtils.getPackageOf(classElement); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / a class member name String variableName = variableElement getSimpleName (), toString (); // Class member type TypeMirror TypeMirror = variableElement.astype (); String type = typeMirror.toString(); }Copy the code

Modify class annotations and TypeElements

for (Element element : RoundEnv. GetElementsAnnotatedWith (BindView. Class)) {/ / ElementType. Strong TYPE annotations can directly turn TypeElement TypeElement classElement = (TypeElement) element; PackageElement packageElement = (PackageElement) element .getEnclosingElement(); . / / the class name String fullClassName = classElement getQualifiedName (), toString (); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / parent class name String superClassName = classElement getSuperclass (), toString (); }Copy the code

The source address

The demo address

reference

Explore annotations in Android quick start annotations

Compile-time annotations for custom annotations

Understand the annotation processor in one hour

Javapoet – Frees you from repetitive boring code

JavaPoet source code cannot be properly imported Modifier class discussion

Android compile time Annotation Framework 5- Syntax explanation