In Plain Android AOP (1), we extend AGP’s Transform API from the Activity’s onPause/onResume log printing requirements. The Transform API implements the same requirement in an AOP manner. To make AOP work for us, there is an implicit flag (or rule) that all Activity onPause/onResume methods print logs. This is the aspect we need. So if we don’t have a marker (or rule, or aspect) in our requirements, can we implement it using AOP ideas? Can!!!! If we don’t mark it, we mark it! This tag is the Annotation Annotation.

Java Annotation

An Annotation is a form of metadata that provides data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. Annotations have many uses, including:

  • Provide information to the compiler – the compiler can use annotations to detect errors or prompt warnings.
  • Compile-time, deploy-time processing – Software tools can process annotation information to generate code, XML files, and so on.
  • Runtime processing – Some types of annotations can still be detected at runtime.

In Android development, we often use annotations like @override @nullable. Let’s take a quick look at what they look like:

// Nullable.java
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
public @interface Nullable {
}
Copy the code

Yuan notes

Now, @documented @Retention @Target is a Nullable annotation. They are indeed annotations, and such annotations that act on annotations are called meta-annotations. Several meta-annotation types are defined in the java.lang.annotation package. @Retention This annotation specifies how to store the annotations marked by it:

Retentionpolicy.source – Marked annotations remain at the SOURCE level only and are ignored by the compiler. Retentionpolicy.class – Marked annotations are retained by the compiler at compile time, but ignored by the Java Virtual Machine (JVM). Retentionpolicy.runtime – Marked annotations are reserved by the JVM for use by the RUNTIME environment.

@documented This annotation indicates that whenever an annotation marked by it is used, these elements should be Documented using the Javadoc tool. (By default, annotations are not included in Javadoc.)

@target This annotation marks another annotation to restrict what type of Java element the tagged annotation can be applied to. The target annotation supports the following element types

Elementtype. ANNOTATION_TYPE can be applied to annotation types. Elementtype. CONSTRUCTOR can be applied to constructors. Elementtype. FIELD can be applied to fields or attributes. Elementtype. LOCAL_VARIABLE can be applied to local variables. Elementtype. METHOD can be applied to function methods. Elementtype. PACKAGE can be applied to PACKAGE declarations. Elementtype. PARAMETER can be applied to method parameters. Elementtype. TYPE can be applied to any element of a class.

@Inherited This annotation indicates that annotation types can be Inherited from their parent (by default, they are not Inherited). When an annotation type is queried and the current class has no corresponding annotation, its parent class is queried for the corresponding annotation. This annotation applies only to class declarations.

Transform With Annotation

Knowing the concept of annotations, we changed the requirement to print the annotated function name, parameter name, and parameter value. First we need to define annotations, then add annotation tags to the functions that need to print logs, then identify annotations through the Transform API and add log print code using ASM.

Custom annotation

Our annotations are used in two places: the Module of the annotated function and the Gradle plugin Module that handles the annotations. So it makes sense to treat annotations as a separate Java Library Module and have both modules rely on the annotation Module. Create a new Java Library-type module and add the annotation code:

// Our Transform API works with.class files, so we need annotations to be preserved by the compiler
@Retention(RetentionPolicy.CLASS)
// Our annotations can only be applied to method functions
@Target({ElementType.METHOD})
public @interface MethodLoggable {
}
Copy the code

Use the annotation marking method

The APP module adds a dependency on the annotation module above, and then adds an annotation mark to the method that needs to print the log:

@MethodLoggable
private void testMethod(String name, int age) {
    int i = 10;
    int j = age + i;
}
Copy the code

Deal with annotations

Create a Gradle Plugin Module. For details about how to create a Gradle Plugin Module, see Gradle Plugin. Then, as with vernacular Android AOP (I), add the ASM dependencies, register the Transform, find all the.class files, and implement the requirements in two steps.

  1. Analytic function parameters

Let’s start by comparing the Java and bytecode code for the next function: LOCALVARIABLERepresents the local variable associated with the function, which contains the names of the function parameters. This information is actually in the functionreturnAfter the statement, there is no way to parse the function arguments and add log-print logic. I’m using a rather clumsy approach here. For each.class file parsed, the first time I fetch method parameter information and the second time I add log printing logic. It is used in actual productionAspectJI’m not familiar with AspectJ, so I’m going to do something stupid, so don’t do it. Well, it doesn’t have to be, becauseJake WhartonThe great God has done it for usGithub.com/JakeWharton…. Method parameter parsing main code, complete code seeLoggableMethodTransform.jav LoggableMethodParser.java

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                 String[] exceptions) {
    // Parameter names can only be obtained from local variables, but local variables contain more than function parameters.
    // We can see how many arguments there are in the Type array
    Type[] types = Type.getArgumentTypes(descriptor);
    MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
    // The first argument to a non-static method is this
    boolean staticMethod = (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC;
    return new ArgumentsReader(mv, staticMethod, name, types);
}

class ArgumentsReader extends MethodVisitor {...@Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        // Iterate over the function annotation, where we can filter out the functions we are comparing
        if (METHOD_LOGGABLE_DESC.equals(descriptor)) {
            loggableMethod = true;
        }
        return super.visitAnnotation(descriptor, visible);
    }
    
    @Override
    public void visitLocalVariable(String name, String descriptor, String signature,
                                   Label start, Label end, int index) {
        super.visitLocalVariable(name, descriptor, signature, start, end, index);
        if(loggableMethod && argumentNames ! =null) {
            // Collect only the function parameter names that are marked
            if (index >= 0&& index < argumentCount) { argumentNames[index] = name; }}}}Copy the code
  1. Insert log print code into the annotated function

Log into the main code, complete code see LoggableMethodTransform. Java LoggableMethodPrinter. Java

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                 String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
    if(methodToArgumentArray ! =null && methodToArgumentArray.containsKey(name)) {
        boolean staticMethod = (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC;
        int offset = staticMethod ? 0 : 1;
        Type[] types = Type.getArgumentTypes(descriptor);
        String[] argumentNames = methodToArgumentArray.get(name);
        final int argumentsCount = argumentNames.length;
        mv.visitLdcInsn("lenebf");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false);
        mv.visitLdcInsn("Invoke method " + name + "(");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        String argumentName;
        String argumentTypeDescriptor;
        for (int index = 0; index < argumentsCount; index++) {
            argumentName = argumentNames[index];
            if ("this".equals(argumentName)) {
                // Exclude this argument
                continue;
            }
            // Parameter name
            mv.visitLdcInsn(argumentName + ":");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
            // The value is described as follows
            argumentTypeDescriptor = types[index - offset].getDescriptor();
            // Read the value of the index parameter
            mv.visitVarInsn(Opcodes.ILOAD, index);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(" + argumentTypeDescriptor + ")Ljava/lang/StringBuilder;".false);
            if(index ! = argumentsCount -1) {
                // Insert non-last item, separate the arguments
                mv.visitLdcInsn(",");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
            }
        }
        mv.visitLdcInsn(")");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false);
        mv.visitInsn(Opcodes.POP);
    }
    return mv;
}
Copy the code

Verify plug-in effects

  1. Look at the code generated by Transform

  1. View the code running results

It’s great. It’s exactly what we expected.

Annotation Processing Tool

Is an Annotation so useful that it has its own Processing Tool, the Annotation Processing Tool (APT)? APT finds and executes Annotation Processor based on annotations existing in specified source files. Annotation Processor modifies source code or generates new source code files according to information provided by Annotation tags. Both modified source code files and newly generated source code files can be compiled. So as to achieve the purpose of modifying the original logic or adding new logic. Butterknife, ARouter, which we often use, are masterpieces using APT techniques. Again, we changed the requirement to be able to get the compile time of the annotated class.

JavaPoet

As we know from the previous introduction, APT uses the Annotation Processor to process annotations. The Annotation Processor is for source code, i.e. Java, so ASM is not applicable. So we need a new tool, JavaPoet. JavaPoet, produced by Square, is a set of apis for generating.java source files. See github.com/square/java… . Square also makes OkHttp, Retrofit, which we all use, which is amazing!

Custom annotation

Create a new Java Library Module and define our annotations:

// Annotation handlers work with.java files, and we only need annotations to be preserved at the source level
@Retention(RetentionPolicy.SOURCE)
// Our annotations apply only to classes
@Target({ElementType.TYPE})
public @interface KeepBuildTime {
}
Copy the code

Annotation Processor

With the code generation tool, let’s implement the annotation processor. The basic logic is simple: create a New Java Library Module, add dependencies on JavaPoet and the above annotation Module, and then define a class that inherits from AbstractProcessor and implements the key methods.

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

    private Elements elementUtils;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        // Get the element handling utility class
        elementUtils = processingEnvironment.getElementUtils();
        // File manager for creating new source files, class files, or auxiliary files
        filer = processingEnvironment.getFiler();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        // Which annotations need to be processed by the annotation processor
        return Collections.singleton(KeepBuildTime.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // Handle annotations
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(KeepBuildTime.class);
        for (Element element : elements) {
            generateBTCLass((TypeElement) element);
        }
        return true;
    }

    private void generateBTCLass(TypeElement element) {
        // Annotated class name + "_BT" as the class name of the utility class
        String btClassName = element.getSimpleName().toString() + "_BT";
        // The annotated package name
        String packageName = elementUtils.getPackageOf(element).toString();
        // Generate a utility class that gets compilation time
        TypeSpec btClass = TypeSpec.classBuilder(btClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                // Generate a method to get the compile time
                .addMethod(generateGetBuildTimeMethod())
                .build();
        JavaFile javaFile = JavaFile.builder(packageName, btClass).build();
        try {
            // Outputs the generated new class
            javaFile.writeTo(filer);
        } catch(IOException e) { e.printStackTrace(); }}private MethodSpec generateGetBuildTimeMethod(a) {
        long buildTime = System.currentTimeMillis();
        // The method is called getBuildTime
        return MethodSpec.methodBuilder("getBuildTime")
                // Methods are static public methods
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                // Return long
                .returns(long.class)
                .addStatement("return " + buildTime + "L") .build(); }}Copy the code

AutoService

What is @autoService (processor.class) in this code? In order for the annotation handler to work, similar to developing the Gradle Plugin, you need a configuration file class that describes the annotation handler’s implementation as follows:

  1. Create a resources/ meta-INF /services folder in the main directory of the annotation handler Module.
  2. In the resources/meta-inf/services directory folder to create javax.mail. The annotation. Processing. Processor file;
  3. In the javax.mail. The annotation. Processing. Processor file is written to annotate the Processor’s full name, our case is com. Lenebf. Android. Buildtime_processor. BuildTimeProcessor;

Gradle Plugin Development Plugin helps us to generate configuration files, annotations processor Development also has a big big help to develop the corresponding tool, that is from Google AutoService. Java annotation processors and other systems use java.util.ServiceLoader to register implementations of known types using meta-INF metadata. However, it is easy for developers to forget to update or properly specify service descriptors. For any class that uses the @AutoService annotation, AutoService generates this metadata for the developer, avoiding typing errors, preventing refactoring errors, and so on.

Access the generated utility classes

The previous annotation handler generates the “_BT” ending utility class from the information provided by the annotation. The static method in the utility class named “getBuildTime” is accessed to obtain the compile time of the corresponding class. We’ll create a New Java Library Module named BuildTime and implement the read logic:

public class BuildTime {

    public static long get(Object object) {
        try {
            String btClassName = object.getClass().getCanonicalName() + "_BT"; Class<? > btClass = Class.forName(btClassName); Method getBuildTimeMethod = btClass.getMethod("getBuildTime");
            return (long) getBuildTimeMethod.invoke(object);
        } catch (Throwable throwable) {
            return -1L; }}}Copy the code

Verify the effect of

Now let app moudle rely on buildtime_annotation, buildtime, buildtime_processor:

// Annotations define module
implementation project(path: ':buildtime_annotation')
// Utility class module
implementation project(path: ':buildtime')
// Annotation handlers are introduced differently
annotationProcessor project(path: ':buildtime_processor')
Copy the code

Then add the @keepBuildTime annotation to the app’s MainActivity and log the build time:

@KeepBuildTime
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("lenebf"."The build time is " + BuildTime.get(this)); }}Copy the code

Then look at the generated utility class and the actual log output: Perfect as always!

conclusion

In practice with AOP methodologies, if there are obvious rules (tags) that can extract aspects, we can work directly with the aspects. If there is no obvious rule (markup) to extract the slice, we can create the slice by adding annotations. The slice created by adding annotations can be either implemented using Transform + ASM for code logic or handled by APT + JavaPoet. Example code address: github.com/lenebf/Andr…