Using Android Annotation Processor to Liberate Labor Force

brief introduction

Annotation Processor is widely used in third-party libraries in android development.
Butterknife, Dagger 2, DBFlow and so on are common.

annotation

There are many Api s for annotations in Java, such as @Override for overriding parent class methods, @Deprecated for abandoned class or method attributes, etc. There are some annotations extensions in android, such as @NonNull, @StringRes, @IntRes, etc.

Automatic Code Generation

In order to improve the efficiency of coding and avoid using reflection extensively at runtime, we use reflection to generate auxiliary classes and methods at compile time for use at runtime.

The main processing steps of annotation processor are as follows:
1. Build in the java compiler
2. The compiler starts executing unexecuted annotation processors
3. Cyclic processing of annotation elements to find classes, methods, or attributes modified by the annotation
4. Generate corresponding classes and write them to files
5. Determine whether all annotation processors have been executed, and if not, proceed to the next annotation processor (go back to step 1)

Example of Butterknife Annotation Processor

Butterknife's annotation processor works as follows:
1. Define a non-private Attribute Variable
2. Adding annotations and IDS to the attribute variable
3. Call the Butterknife.bind(.)) method.

When you click the Build button of Android Studio, Butterknife first generates the corresponding auxiliary classes and methods according to the above steps. When the code executes to the bind() method, Butterknife calls the auxiliary class method generated before, and completes the assignment of the annotated element.

Custom Annotation Processor

After understanding the basic knowledge, we should try to use these skills.
Next is practice time. Let's develop a simple example of using annotation processors to automatically generate random numbers and strings.
1. First create a project.
2. Create lib_annotations, a pure java module that does not contain any android code and is only used to store annotations.
3. Create lib_compiler, which is also a pure java module. The module relies on the module_annotation created in Step 2, where all the code for processing annotations is located, and the moduule will not eventually be packaged into the apk, so you can import any size dependency libraries you want here.
4. Create lib_api without requiring the module, which can be android library or java library or others. This module is used to call the auxiliary class method generated in step 3.

1. Add notes

Two annotations are added to lib_annotations: Random String and Random Int to generate random numbers and strings, respectively:

@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomString {
}
@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomInt {
    int minValue() default 0;
    int maxValue() default 65535;
}
  • @interface
    Custom annotations, using @interface as class name modifier
  • @Target
    The optional types of elements that the annotation can modify are as follows:
public enum ElementType {
    TYPE, //class
    FIELD, //attribute
    METHOD, //Method
    PARAMETER, //parameter
    CONSTRUCTOR, //Constructor
    LOCAL_VARIABLE, 
    ANNOTATION_TYPE,
    PACKAGE,
    TYPE_PARAMETER,
    TYPE_USE;

    private ElementType() {
    }
}
  • @Retention
    There are three options for the retention strategy of this annotation:
public enum RetentionPolicy {
    SOURCE, //Ignored by the compiler

    CLASS, //The compiled file is retained to the class file, but not to the runtime

    RUNTIME //Retain to class files and to runtime, reflecting the object modified by the annotation at runtime
}

2. Annotation Processor

All the operations that actually process annotations and generate code are here.
Before writing code, we need to import two important libraries and our annotation module:

compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.9.0'
implementation project(':lib_annotations')

New class RandomProcessor.java:

@AutoService(Processor.class)
public class RandomProcessor extends AbstractProcessor{

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
  • @AutoService
    @ AutoService(Processor.class) tells the compiler that the annotation processor exists and automatically generates a javax.annotation.processing.Processor file under META-INF/services at compile time. The content of the file is
com.rhythm7.lib_compiler.RandomProcessor

That is to say, the annotation processor you declare will be written to this configuration file.
In this way, when the external program loads the module, the implementation class name of the annotation processor can be found through META-INF/services under the jar package of the module, and the module injection can be completed by loading the instantiation.
Annotation processor needs to implement AbstractProcessor interface and implement corresponding methods
- init() is optional
In this method, the processingEnvironment object can be obtained, by which the file object, debug output object and some related tool classes can be obtained.

  • getSupportedSourceVersion()
    Return to the supported version of java, and generally return to the latest version of Java currently supported

  • getSupportedAnnotationTypes()
    For all the comments you need to process, the return value of this method will be received by the process() method.

  • process() must be implemented
    Scan all annotated elements, process them, and finally generate files. The return value of this method is boolean type. If true is returned, the annotations representing this process have been processed. The next annotation processor is not expected to continue processing, otherwise the next annotation processor will continue processing.

Initialization

The more detailed code is as follows:

private static final List<Class<? extends Annotation>> RANDOM_TYPES
        = Arrays.asList(RandomInt.class, RandomString.class);

private Messager messager;
private Types typesUtil;
private Elements elementsUtil;
private Filer filer;

private TypeonProcess()per.init(processingEnv);
    messager = processingEnv.getMessager(); 
    typesUtil = processingEnv.getTypeUtils();
    elementsUtil = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
}

@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotations = new LinkedHashSet<>();

    for (Class<? extends Annotation> annotation : RANDOM_TYPES) {
        annotations.add(annotation.getCanonicalName());
    }
    return annotations;
}

Processing notes

Do the following in the process() method:
1. Scanning all annotation elements and judging the type of annotation elements

for (Element element : roundEnv.getElementsAnnotatedWith(RandomInt.class)) {
    //Annotated Random Int is a simple encapsulation of Elment annotated by Random Int
    AnnotatedRandomInt randomElement = new AnnotatedRandomInt(element);
    messager.printMessage(Diagnostic.Kind.NOTE, randomElement.toString());
    //Determine whether the type of annotation meets the requirements
    if (!element.asType().getKind().equals(TypeKind.INT)) { 
        messager.printMessage(Diagnostic.Kind.ERROR, randomElement.getSimpleClassName().toString() + "#"
          + randomElement.getElementName().toString() + " is not in valid type int");
    }

    //The annotated element is stored in Map by the full class name key of the class in which the annotated element is located, and then class files are generated based on the key.
    String qualifier = randomElement.getQualifiedClassName().toString();
    if (annotatedElementMap.get(qualifier) == null) {
        annotatedElementMap.put(qualifier, new ArrayList<AnnotatedRandomElement>());
    }
    annotatedElementMap.get(qualifier).add(randomElement);
}

Generate class files

Traverse the previous map with the annotated class as the key, and group the class file with the key value.

for (Map.Entry<String, List<AnnotatedRandomElement>> entry : annotatedElementMap.entrySet()) {
    MethodSpec constructor = createConstructor(entry.getValue());
    TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
    JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
    javaFile.writeTo(filer);
}

Generating classes, constructors, code snippets, and files are all made use of javapoet dependency libraries. Of course, you can also choose to splice strings and write them with file IO, but using javapoet is much more convenient.

private MethodSpec createConstructor(List<AnnotatedRandomElement> randomElements) {
    AnnotatedRandomElement firstElement = randomElements.get(0);
    MethodSpec.Builder builder = MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(TypeName.get(firstElement.getElement().getEnclosingElement().asType()), "target");
    for (int i = 0; i < randomElements.size(); i++) {
        addStatement(builder, randomElements.get(i));
    }
    return builder.build();
}

private void addStatement(MethodSpec.Builder builder, AnnotatedRandomElement randomElement) {
    builder.addStatement(String.format(
            "target.%1$s = %2$s",
            randomElement.getElementName().toString(),
            randomElement.getRandomValue())
    );
}

private TypeSpec createClass(String className, MethodSpec constructor) {
    return TypeSpec.classBuilder(className + "_Random")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(constructor)
            .build();
}

private String getPackage(String qualifier) {
    return qualifier.substring(0, qualifier.lastIndexOf("."));
}

private String getClassName(String qualifier) {
    return qualifier.substring(qualifier.lastIndexOf(".") + 1);
}

Through the above lines of code, the class file is created. Add a parameter (target) to the constructor of the class, and add the statement "target.%1s=s=s" to each annotated element. Finally, write the file through javaFile.writeTo(filer).

3. Call the method that generates the class

Create a new class in lib_api: RandomUtil.java, add an injection method:

public static void inject(Object object) {
    Class bindingClass = Class.forName(object.getClass().getCanonicalName() + "_Random"); 
    Constructor constructor = bindingClass.getConstructor(object.getClass());
    constructor.newInstance(object);
}

Here we use reflection to find the generated class named "Object class name _Random" and call its construction method. In our previous annotation processor, we have implemented the assignment of attributes in the construction method of generated classes.

4. Use generated classes

Depend on the library you just created in app module:

implementation project(':lib_annotations')
implementation project(':lib_api')
annotationProcessor project(':lib_compiler')

Use in Activity

public class MainActivity extends AppCompatActivity {
    @RandomInt(minValue = 10, maxValue = 1000)
    int mRandomInt;

    @RandomString
    String mRandomString;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RandomUtil.inject(this);

        Log.i("RandomInt ==> ", mRandomInt + "");
        Log.i("RandomString ==> ", mRandomString);
    }
}

Compile, run, and view the results:

02-11 18:38:51.747 7887-7887/com.rhythm7.annotationprocessordemo I/RandomInt ==>: 700
02-11 18:38:51.747 7887-7887/com.rhythm7.annotationprocessordemo I/RandomString ==>: HhRayFyTtt

The annotated element is successfully assigned automatically, indicating that the injection is successful.

Full demo address: github

Original address: http://www.unfishable.com

debugging

The debug of the annotation processor is somewhat different from that of the normal code debug:

Enter commands under the current engineering path

gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

And add a remote configuration in Edit Configurations with a random name and a port of 5005.
Then click the debug button to connect to the remote debugger for Annotation debugging.

Keywords: Java ButterKnife Android Attribute

Added by Dark_Storm on Sat, 18 May 2019 20:03:52 +0300