“I have given up my dream of becoming the hairiest programmer in the Pearl River Delta…”

Introduction to the

In Android development, many of the most commonly used third-party libraries use Annotation processors. Common examples are Butterknife, Dagger2, DBFlow, etc.

annotations

There are many apis for annotations in Java, such as @Override to Override a parent class method and @deprecated to indicate a Deprecated class or method attribute. There are also some annotation extensions in Android, such as @nonNULL, @stringres, @Intres, etc.

Automatic code generation

Automatic code generation is used both to improve coding efficiency and to avoid extensive use of reflection at run time, by using reflection at compile time to generate helper classes and methods for use at run time.

Annotation processor processing steps are as follows:

  1. Build in the Java compiler
  2. The compiler starts executing the unexecuted annotation handler
  3. Loop through the annotation Element to find the class, method, or property that the annotation modifies
  4. Generate the corresponding class and write to the file
  5. Determine if all annotation handlers have completed execution, if not, proceed to the next annotation handler (back to Step 1)

An example of a Butterknife annotation processor

Butterknife’s annotation processor works like this:

  1. Define a non-private attribute variable
  2. Add an annotation and pass in the ID for the attribute variable
  3. callButterknife.bind(..)Methods.

When you click the Build button in Android Studio, Butterknife first generates the corresponding helper classes and methods following the steps described above. In code execution to bind(..) Method, Butterknife calls the previously generated helper class method to complete the assignment of the annotated element.


Custom annotation handlers

Once you know the basics, you should try to use these techniques. Next up in practice, let’s develop a simple example that uses an annotation processor to automatically generate random numbers and strings.

  1. Start by creating a project.
  2. Create lib_annotations, a pure Java module that doesn’t contain any Android code and is only used to store annotations.
  3. Create lib_compiler, which is also a pure Java module. This module relies on the module_annotation created in Step 2. The code that handles the annotations is there, and the Moduule won’t end up packaged into APK, so you can import any dependency library you want there.
  4. Create lib_API, no requirements for this module, android Library or Java Library or whatever. This Module is used to invoke the helper class methods generated in Step 3.

1. Add annotations

Annotations add two annotations to lib_annotations (RandomString, RandomInt) to generate random numbers and strings, respectively:

@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomString {
}
Copy the code
@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomInt {
    int minValue(a) default 0;
    int maxValue(a) default 65535;
}
Copy the code
  • @interface Custom annotation using @interface as the class name modifier
  • @target Specifies the type of element that can be modified by this annotation. The options are as follows:
public enum ElementType {
    TYPE, / / class
    FIELD, / / property
    METHOD, / / method
    PARAMETER, / / parameters
    CONSTRUCTOR, // constructor
    LOCAL_VARIABLE, 
    ANNOTATION_TYPE,
    PACKAGE,
    TYPE_PARAMETER,
    TYPE_USE;

    private ElementType(a) {}}Copy the code
  • @Retention Retention policy for this annotation, which has three options:
public enum RetentionPolicy {
    SOURCE, // Ignored by the compiler

    CLASS, // Is saved by the compiler to a class file, but not to runtime

    RUNTIME // To the class file, and to runtime, which can reflect the object modified by the annotation at runtime
}
Copy the code

2. Annotation processor

The actual handling of the annotations and generating the code is all here. Before writing the code we need to import two important libraries and our annotation module:

compile 'com. Google. Auto. Services: auto - service: 1.0 rc4'
compile 'com. Squareup: javapoet: 1.9.0'
implementation project(':lib_annotations')
Copy the code

The 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(a) {
        return super.getSupportedSourceVersion();
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false; }}Copy the code
  • @AutoService@autoService (processor.class) tells the compiler of the existence of the annotation handler and automatically registers it at compile timeMETA-INF/servicesNext generationjavax.annotation.processing.ProcessorFile, the content of the file is
com.rhythm7.lib_compiler.RandomProcessor
Copy the code

That is, any annotation handler you declare will be written to the configuration file. This way, when an external application loads the module, it can find the implementation class name of the annotation handler in the meta-INF /services jar of the module and load the instantiation to complete the module injection. The annotation processor needs to implement the AbstractProcessor interface and implement the corresponding methods

  • Init () optionally gets the processingEnvironment object, the file object from which the code is generated, the debug output object, and some related utility classes

  • GetSupportedSourceVersion () returns the supported Java version, returns the current commonly supported by the latest Java version

  • GetSupportedAnnotationTypes () what you need to deal with all the comments, the return value of this method will be the process () method receives

  • Process () must implement scanning all annotated elements, processing them, and finally generating the file. The return value of this method is Boolean. True means that all annotations have been processed and the next annotation handler is not expected to continue processing. Otherwise, the next annotation handler will continue processing.

Initialize the

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(a)per.init(processingEnv);
    messager = processingEnv.getMessager(); 
    typesUtil = processingEnv.getTypeUtils();
    elementsUtil = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
}

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

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

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

Deal with annotations

In the process() method do the following:

  1. Scan all annotation elements and determine the type of annotation elements
for (Element element : roundEnv.getElementsAnnotatedWith(RandomInt.class)) {
    //AnnotatedRandomInt is a simple encapsulation of Elment annotated with RandomInt
    AnnotatedRandomInt randomElement = new AnnotatedRandomInt(element);
    messager.printMessage(Diagnostic.Kind.NOTE, randomElement.toString());
    // Determine whether the annotated type 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 the Map according to the full class name of the annotated element
    String qualifier = randomElement.getQualifiedClassName().toString();
    if (annotatedElementMap.get(qualifier) == null) {
        annotatedElementMap.put(qualifier, new ArrayList<AnnotatedRandomElement>());
    }
    annotatedElementMap.get(qualifier).add(randomElement);
}
Copy the code

Generating class files

Iterate through the previous map with the annotation class as the key and generate class files grouped by 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);
}
Copy the code

Classes, constructors, code snippets, and files are generated using javapoet dependent libraries. Of course you can also choose to concatenate strings and write in file IO yourself, 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);
}
Copy the code

With the above few lines of code, the class file is created. Add a parameter (target) to the constructor of the class and add a statement “target.%1$s = %2$s” to each annotated element. Finally, writeTo the file via javafile.writeto (filer).

3. Call the method that generated the class

Create a new class in lib_API: randomUtil.java and 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);
}
Copy the code

Here reflection is used to find the generated class named “Object class name _Random” and call its constructor. In our previous annotation handler, we implemented the assignment of attributes in the constructor of the generated class.

4. Use generated classes

Rely on the library you just created in the App Module:

implementation project(':lib_annotations')
implementation project(':lib_api')
annotationProcessor project(':lib_compiler')
Copy the code

Use in an 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); }}Copy the code

Compile, run, and view the results:

RandomInt ==>: 700
RandomString ==>: HhRayFyTtt
Copy the code

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

Full demo address

The original address


debugging

Annotation handler debug is a little different from regular code debug:

Enter the command in the current project path

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

And add a new remote configuration (Remote) with an arbitrary name and port 5005 in Edit Configurations. Then click the Debug button to connect to the remote debugger for Annotation debugging.