preface

There are generally two purposes for defining annotations, the second of which is explained in the section following the introduction of this article.

  • The first kind: Android annotations using the enumeration of the introduction to define annotations, for data type and scope check, more work in the source code editing stage, is the IDE provides power functions.

  • The second kind: data injection and code generation, in the development process to reduce boilerplate code, optimize readability, help improve work efficiency and so on.

Structure of the annotation library

An annotation tool, generally divided into two modules: annotation definition module and annotation processor module. Take ButterKnife as an example:

  • Add annotations to define modules
implementation 'com. Jakewharton: butterknife: 8.4.0'
Copy the code
  • Add the annotation handler module

Annotation processors are typically added using annotationProcessor or kapt.

kapt 'com. Jakewharton: butterknife: 8.4.0'                    // Support kotlin source code
annotationProcessor 'com. Jakewharton: butterknife: 8.4.0'     // Support Java source code
Copy the code

Custom annotation implementation

The annotation example is used to implement data injection.

Annotations to define

All annotations default inherited from Java. Lang, the annotation. The annotation. Type constraints for Annotation members: 8 basic data types, String, Class, Annotation and subclass, enumeration.

When defining an annotation, you can declare 0.. N members, as defined below, can be specified by default for members; Member names can be set according to the programming language’s variable naming rules.

// Target Specifies the scope of the StringIntentKey.
@Target(ElementType.FIELD)
// If you use dynamic annotations, you need to specify Retention as RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface StringIntentKey {
    String value(a) default "";
}
Copy the code

Annotations to call

Once the annotation definition is introduced, it is ready to be called. You need to specify the parameter name. If the parameter name is value and there is only one member, you can omit it.

@ StringAnnotation (value = “data”),

@StringIntentKey("dynamic_data")
String dynamicData;
@StringIntentKey("static_data")
String staticData;
Copy the code

Annotation processing

For annotations to really work for data injection and code generation, it is the annotation processor that identifies and processes the annotation location. Annotation processors come in two types: dynamic and static.

  • Dynamic annotation processing, the use of reflection technology, by scanning member variables, methods, etc., to identify the annotation call after the assignment operation. By dynamic, I mean that operations scan code dynamically at run time, so there is a performance cost.
public class DynamicUtil {
    public static void inject(Activity activity) {
        Intent intent = activity.getIntent();
        / / reflection
        for (Field field : activity.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(StringIntentKey.class)) {
                // Get the annotation
                StringIntentKey annotation = field.getAnnotation(StringIntentKey.class);
                String intentKey = annotation.value();
                // Read the actual IntentExtra value
                Serializable serializable = intent.getSerializableExtra(intentKey);
                if (serializable == null) {
                    if (field.getType().isAssignableFrom(String.class)) {
                        serializable = ""; }}try {
                    / / insert value
                    boolean accessible = field.isAccessible();
                    field.setAccessible(true);
                    field.set(activity, serializable);
                    field.setAccessible(accessible);
                } catch(IllegalAccessException e) { e.printStackTrace(); }}}}Copy the code
  • Static annotation processing, as opposed to dynamic annotation processing. Annotation identification and processing is done at compile time without code scanning at runtime, thus saving runtime performance costs but increasing compile time.

Static annotation processing, which requires scanning source files at compile time to generate new source files, is explained separately in the following section code generation.

Code generation

Static annotations appear after and replace dynamic annotations. Static annotations, as opposed to dynamic annotations, place the interpretation of annotations at compile time and use the compiled results directly instead of the interpretation at run time. Therefore, the compilation phase needs to use the appropriate tools to generate the required code.

  1. Javapoet: A Java Library for generating Java code, see article.
  2. Auto-service:github.com/google/auto…

In this example, generate a Binder class for each annotated class and provide a unified entry for StaticMapper#bind for data injection. All you need to do is call this method at initialization.

public final class MainActivity$Binder {
    public static final void bind(MainActivity activity) {
        Intent intent = activity.getIntent();
        if (intent.hasExtra("static_data")) {
           activity.staticData = (String) intent.getSerializableExtra("static_data"); }}}Copy the code
public final class StaticMapper {
    public static final void bind(Activity activity) {
        if (activity instanceof MainActivity) {
           MainActivity$Binder binder = new MainActivity$Binder();
           binder.bind((com.campusboy.annotationtest.MainActivity) activity);
        } else if (activity instanceof Main2Activity) {
           Main2Activity$Binder binder = newMain2Activity$Binder(); binder.bind((com.campusboy.annotationtest.Main2Activity) activity); }}}Copy the code
  • The annotation interpreter needs to inherit from the AbstractProcessor base class and declare this class to be an annotation Processor using @AutoService(processor.class).

import com.google.auto.service.AutoService;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;

@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {}Copy the code

public abstract class AbstractProcessor implements Processor {}Copy the code
  • AbstractProcessor base class AbstractProcessor implements the Processor interface, where init() and getSupportedOptions() are implemented in AbstractProcessor. The main function of StaticIntentProcessor is to implement the process() method to generate the class.
public interface Processor {
    Set<String> getSupportedOptions(a);
    // A collection of class names for supported annotation classes
    Set<String> getSupportedAnnotationTypes(a);
    // The supported Java version
    SourceVersion getSupportedSourceVersion(a);

    void init(ProcessingEnvironment var1);

    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}
Copy the code
  • Here is the static annotation processing in this example, identifying the annotation call and generating the corresponding code file. No longer need to be obtained by reflection at runtime.
@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {

    private TypeName activityClassName = ClassName.get("android.app"."Activity").withoutAnnotations();
    private TypeName intentClassName = ClassName.get("android.content"."Intent").withoutAnnotations();

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        / / support java1.7
        return SourceVersion.RELEASE_7;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        // Only the StringIntentKey annotation is handled
        return Collections.singleton(StringIntentKey.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
        // StaticMapper's bind method
        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityClassName, "activity");

        // Find all classes that need to be injected
        List<InjectDesc> injectDescs = findInjectDesc(set, re);

        for (int i1 = 0; i1 < injectDescs.size(); i1++) {
            InjectDesc injectDesc = injectDescs.get(i1);

            // Create a Java file for the class to be annotated, as described above in IntentActivity$Binder
            TypeName injectedType = createInjectClassFile(injectDesc);
            TypeName activityName = typeName(injectDesc.activityName);

            // $T import type
            // Generate code for binding distribution
            method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName);
            method.addCode("\t$T binder = new $T(); \n", injectedType, injectedType);
            method.addCode("\tbinder.bind((" + activityName + ") activity); \n", activityName, activityName);
            method.addCode("}");
        }
        // Create the StaticMapper class
        createJavaFile("com.campusboy.annotationtest"."StaticMapper", method.build());

        return false;
    }

    private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {

        Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();

        // Get all elements marked by StringIntentKey
        Set<? extends Element> elements = re.getElementsAnnotatedWith(StringIntentKey.class);
        for (Element element : elements) {
            // Only care about elements whose categories are attributes
            if(element.getKind() ! = ElementKind.FIELD) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"only support field");
                continue;
            }

            // The description type of the class is found here
            // Because our StringIntentKey's annotation description is Field, the closingElement element is class
            TypeElement classType = (TypeElement) element.getEnclosingElement();

            System.out.println(classType);

            // Cache the class to avoid duplication
            List<String[]> nameList = targetClassMap.get(classType);
            if (nameList == null) {
                nameList = new ArrayList<>();
                targetClassMap.put(classType, nameList);
            }

            // Annotated value, such as staticName
            String fieldName = element.getSimpleName().toString();
            // The type of the annotated value, e.g. String, int
            String fieldTypeName = element.asType().toString();
            // The value of the annotation itself, such as key_name
            String intentName = element.getAnnotation(StringIntentKey.class).value();

            String[] names = new String[]{fieldName, fieldTypeName, intentName};
            nameList.add(names);
        }

        List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size());
        for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) {
            String className = entry.getKey().getQualifiedName().toString();
            System.out.println(className);

            // Encapsulate to a custom descriptor
            InjectDesc injectDesc = new InjectDesc();
            injectDesc.activityName = className;
            List<String[]> value = entry.getValue();
            injectDesc.fieldNames = new String[value.size()];
            injectDesc.fieldTypeNames = new String[value.size()];
            injectDesc.intentNames = new String[value.size()];
            for (int i = 0; i < value.size(); i++) {
                String[] names = value.get(i);
                injectDesc.fieldNames[i] = names[0];
                injectDesc.fieldTypeNames[i] = names[1];
                injectDesc.intentNames[i] = names[2];
            }
            injectDescList.add(injectDesc);
        }

        return injectDescList;
    }

    private void createJavaFile(String pkg, String classShortName, MethodSpec... method) {
        TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
        for (MethodSpec spec : method) {
            builder.addMethod(spec);
        }
        TypeSpec clazzType = builder.build();

        try {
            JavaFile javaFile = JavaFile.builder(pkg, clazzType)
                    .addFileComment(" This codes are generated automatically. Do not modify!")
                    .indent("")
                    .build();
            // write to file
            javaFile.writeTo(processingEnv.getFiler());
        } catch(IOException e) { e.printStackTrace(); }}private TypeName createInjectClassFile(InjectDesc injectDesc) {

        ClassName activityName = className(injectDesc.activityName);
        ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder");

        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityName, "activity");

        $T import as class, $N import as pure value, $S import as string
        method.addStatement("$T intent = activity.getIntent()", intentClassName);
        for (int i = 0; i < injectDesc.fieldNames.length; i++) {
            TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]);
            method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]);
            method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S); \n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]);
            method.addCode("}\n");
        }

        // Generate the final XXX$Binder file
        createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build());

        return injectedClass;
    }

    private TypeName typeName(String className) {
        return className(className).withoutAnnotations();
    }

    private ClassName className(String className) {

        // Base type descriptor
        if (className.indexOf(".") < =0) {
            switch (className) {
                case "byte":
                    return ClassName.get("java.lang"."Byte");
                case "short":
                    return ClassName.get("java.lang"."Short");
                case "int":
                    return ClassName.get("java.lang"."Integer");
                case "long":
                    return ClassName.get("java.lang"."Long");
                case "float":
                    return ClassName.get("java.lang"."Float");
                case "double":
                    return ClassName.get("java.lang"."Double");
                case "boolean":
                    return ClassName.get("java.lang"."Boolean");
                case "char":
                    return ClassName.get("java.lang"."Character");
                default:}}// Manually parse java.lang.String into java.lang package name and String class name
        String packageD = className.substring(0, className.lastIndexOf('. '));
        String name = className.substring(className.lastIndexOf('. ') + 1);
        return ClassName.get(packageD, name);
    }

    private static class InjectDesc {
        private String activityName;
        private String[] fieldNames;
        private String[] fieldTypeNames;
        private String[] intentNames;

        @Override
        public String toString(a) {
            return "InjectDesc{" +
                    "activityName='" + activityName + '\' ' +
                    ", fieldNames=" + Arrays.toString(fieldNames) +
                    ", intentNames=" + Arrays.toString(intentNames) +
                    '} '; }}}Copy the code

Afterword.

In this example, a StaticMapper is generated in the APP module, but in a multi-module project, multiple StaticMapper class files will be generated, resulting in compilation exceptions and failure to complete the compilation process. This problem is left to be solved later.

The sample project

Example project: customize-Annotation

Code generation library: javaPoet uses this library to make code generation easier.