Hand rolling an annotation frame

Keywords: Android Java Gradle github

Runtime annotation is mainly implemented by reflection, while compile time annotation helps us to generate code during compilation, so compile time annotation is efficient, but it is a little more complex to implement. Runtime annotation is inefficient, but it is simple to implement.
Let's first look at how runtime annotations are implemented.

1. Runtime notes

1.1 definition notes

First, two runtime annotations are defined, in which Retention indicates that the annotation takes effect at runtime, and Target indicates the program element scope of the annotation. The following two examples, RuntimeBindView, are used to describe member variables and classes. Member variables are bound to view, and classes are bound to layout. RuntimeBindClick is used to describe methods, and the specified view is bound to the click event.

@Retention(RetentionPolicy.RUNTIME)//Effective at runtime
@Target({ElementType.FIELD,ElementType.TYPE})//Describe variables and classes
public @interface RuntimeBindView {
    int value() default View.NO_ID;
}

@Retention(RetentionPolicy.RUNTIME)//Effective at runtime
@Target(ElementType.METHOD)//description method
public @interface RuntimeBindClick {
    int[] value();
}

1.2 reflection implementation

The following code is an annotation function implemented by reflection, in which ClassInfo is a tool class that can parse various members and methods of the class,
Source code: https://github.com/huangbei1990/HDemo/blob/master/hutils/src/main/java/com/android/hutils/reflect/ClassInfo.java
In fact, the logic is very simple, that is, take out the specified annotation from the activity, and then call the corresponding method, such as take out the annotation of the RuntimeBindView description class, then get the return value of the annotation, and then call the setContentView of activity to set the layout id.

public static void bindId(Activity obj){
    ClassInfo clsInfo = new ClassInfo(obj.getClass());
    //Processing class
    if(obj.getClass().isAnnotationPresent(RuntimeBindView.class)) {
        RuntimeBindView bindView = (RuntimeBindView)clsInfo.getClassAnnotation(RuntimeBindView.class);
        int id = bindView.value();
        clsInfo.executeMethod(clsInfo.getMethod("setContentView",int.class),obj,id);
    }

    //Process class members
    for(Field field : clsInfo.getFields()){
        if(field.isAnnotationPresent(RuntimeBindView.class)){
            RuntimeBindView bindView = field.getAnnotation(RuntimeBindView.class);
            int id = bindView.value();
            Object view = clsInfo.executeMethod(clsInfo.getMethod("findViewById",int.class),obj,id);
            clsInfo.setField(field,obj,view);
        }
    }

    //Handle click events
    for (Method method : clsInfo.getMethods()) {
        if (method.isAnnotationPresent(RuntimeBindClick.class)) {
            int[] values = method.getAnnotation(RuntimeBindClick.class).value();
            for (int id : values) {
                View view = (View) clsInfo.executeMethod(clsInfo.getMethod("findViewById", int.class), obj, id);
                view.setOnClickListener(v -> {
                    try {
                        method.invoke(obj, v);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
}

1.3 use

As shown below, write the annotations we have defined to the corresponding locations, and then call the bind function of BindApi. Very simple.

@RuntimeBindView(R.layout.first)//class
public class MainActivity extends AppCompatActivity {

    @RuntimeBindView(R.id.jump)//member
    public Button jump;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        BindApi.bindId(this);//Call reflection
    }

    @RuntimeBindClick({R.id.jump,R.id.jump2})//Method
    public void onClick(View view){
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
}

2. Compile time comments

Compile time annotation is to help you generate code automatically during compilation. In fact, the principle is not difficult.

2.1 definition notes

We can see that the Retention value is different when the compile time annotation is defined from the runtime annotation.

@Retention(RetentionPolicy.CLASS)//Effective at compile time
@Target({ElementType.FIELD,ElementType.TYPE})//Describe variables and classes
public @interface CompilerBindView {
    int value() default -1;
}

@Retention(RetentionPolicy.CLASS)//Effective at compile time
@Target(ElementType.METHOD)//description method
public @interface CompilerBindClick {
    int[] value();
}

2.2 generate code based on comments

1) Preparations

First, we need to create a new java lib library, because we need to inherit the AbstractProcessor class, which is not in Android.

Then we need to introduce two packages. Javapot is the package to help us generate code, and auto service is the package to help us automatically generate META-INF and other information, so that when we compile, we can execute our custom processor.

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api 'com.squareup:javapoet:1.9.0'
    api 'com.google.auto.service:auto-service:1.0-rc2'
}


sourceCompatibility = "1.8"
targetCompatibility = "1.8"

2) Inherit AbstractProcessor

As shown below, we need to customize a class to inherit the AbstractProcessor and copy its methods, and annotate it with AutoService.
ClassElementsInfo is a class used to store class information. This step will not be taken care of for the time being. The next step will be explained in detail.
In fact, we can see what it means from the name of the function. init initialization, getSupportedSourceVersion limit the supported jdk version, getSupportedAnnotationTypes need to process annotations. In this function, we can get the classes that we need to process annotations, and generate the corresponding code.

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

    private Filer mFileUtils;//File related auxiliary class, responsible for generating java code
    private Elements mElementUtils;//Auxiliary class related to element to obtain information related to element
    private Map<String,ClassElementsInfo> classElementsInfoMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFileUtils = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
        classElementsInfoMap = new HashMap<>();
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(CompilerBindClick.class.getCanonicalName());
        set.add(CompilerBindView.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        classElementsInfoMap.clear();
        //1. Collect required information
        collection(roundEnvironment);
        //2. Generate specific code
        generateClass();
        return true;
    }

3) Collect comments

First, let's take a look at the ClassElementsInfo class, which is the information we need to collect.
TypeElement is a class element, VariableElement is a member element, ExecutableElement is a method element, from which we can get various annotation information.
classSuffix is the prefix. For example, the original class is MainActivity, and the class name generated by annotation is MainActivity+classSuffix

public class ClassElementsInfo {

    //class
    public TypeElement mTypeElement;
    public int value;
    public String packageName;

    //Member, key is id
    public Map<Integer,VariableElement> mVariableElements = new HashMap<>();

    //Method, key is id
    public Map<Integer,ExecutableElement> mExecutableElements = new HashMap<>();

    //Suffix
    public static final String classSuffix = "proxy";

    public String getProxyClassFullName() {
        return mTypeElement.getQualifiedName().toString() + classSuffix;
    }
    public String getClassName() {
        return mTypeElement.getSimpleName().toString() + classSuffix;
    }
    ......
}

Then we can start to collect annotation information,
As shown below, according to the collection of annotation types one by one, you can get annotation elements through the roundenvironment.getelementsnannotatedwith function, and then fill them into ClassElementsInfo according to the annotation element types.
Where ClassElementsInfo is stored in the Map, key is String and classPath.

private void collection(RoundEnvironment roundEnvironment){
    //1. Collect compileBindView annotations
    Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(CompilerBindView.class);
    for(Element element : set){
        //1.1 notes of collection class
        if(element.getKind() == ElementKind.CLASS){
            TypeElement typeElement = (TypeElement)element;
            String classPath = typeElement.getQualifiedName().toString();
            String className = typeElement.getSimpleName().toString();
            String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
            CompilerBindView bindView = element.getAnnotation(CompilerBindView.class);
            if(bindView != null){
                ClassElementsInfo info = classElementsInfoMap.get(classPath);
                if(info == null){
                    info = new ClassElementsInfo();
                    classElementsInfoMap.put(classPath,info);
                }
                info.packageName = packageName;
                info.value = bindView.value();
                info.mTypeElement = typeElement;
            }
        }
        //1.2 collect member's comments
        else if(element.getKind() == ElementKind.FIELD){
            VariableElement variableElement = (VariableElement) element;
            String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
            CompilerBindView bindView = variableElement.getAnnotation(CompilerBindView.class);
            if(bindView != null){
                ClassElementsInfo info = classElementsInfoMap.get(classPath);
                if(info == null){
                    info = new ClassElementsInfo();
                    classElementsInfoMap.put(classPath,info);
                }
                info.mVariableElements.put(bindView.value(),variableElement);
            }
        }
    }

    //2. Collect compileBindClick comments
    Set<? extends Element> set1 = roundEnvironment.getElementsAnnotatedWith(CompilerBindClick.class);
    for(Element element : set1){
        if(element.getKind() == ElementKind.METHOD){
            ExecutableElement executableElement = (ExecutableElement) element;
            String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
            CompilerBindClick bindClick = executableElement.getAnnotation(CompilerBindClick.class);
            if(bindClick != null){
                ClassElementsInfo info = classElementsInfoMap.get(classPath);
                if(info == null){
                    info = new ClassElementsInfo();
                    classElementsInfoMap.put(classPath,info);
                }
                int[] values = bindClick.value();
                for(int value : values) {
                    info.mExecutableElements.put(value,executableElement);
                }
            }
        }
    }
}

4) Generate code

As shown below, using java pot to generate code is not complicated.

public class ClassElementsInfo {
    ......
    public String generateJavaCode() {
        ClassName viewClass = ClassName.get("android.view","View");
        ClassName clickClass = ClassName.get("android.view","View.OnClickListener");
        ClassName keepClass = ClassName.get("android.support.annotation","Keep");
        ClassName typeClass = ClassName.get(mTypeElement.getQualifiedName().toString().replace("."+mTypeElement.getSimpleName().toString(),""),mTypeElement.getSimpleName().toString());

        //Construction method
        MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(typeClass,"host",Modifier.FINAL);
        if(value > 0){
            builder.addStatement("host.setContentView($L)",value);
        }

        //member
        Iterator<Map.Entry<Integer,VariableElement>> iterator = mVariableElements.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<Integer,VariableElement> entry = iterator.next();
            Integer key = entry.getKey();
            VariableElement value = entry.getValue();
            String name = value.getSimpleName().toString();
            String type = value.asType().toString();
            builder.addStatement("host.$L=($L)host.findViewById($L)",name,type,key);
        }

        //Method
        Iterator<Map.Entry<Integer,ExecutableElement>> iterator1 = mExecutableElements.entrySet().iterator();
        while(iterator1.hasNext()){
            Map.Entry<Integer,ExecutableElement> entry = iterator1.next();
            Integer key = entry.getKey();
            ExecutableElement value = entry.getValue();
            String name = value.getSimpleName().toString();
            MethodSpec onClick = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(viewClass,"view")
                    .addStatement("host.$L(host.findViewById($L))",value.getSimpleName().toString(),key)
                    .returns(void.class)
                    .build();
            //Construct anonymous inner class
            TypeSpec clickListener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(clickClass)
                    .addMethod(onClick)
                    .build();
            builder.addStatement("host.findViewById($L).setOnClickListener($L)",key,clickListener);
        }

        TypeSpec typeSpec = TypeSpec.classBuilder(getClassName())
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(keepClass)
                .addMethod(builder.build())
                .build();
        JavaFile javaFile = JavaFile.builder(packageName,typeSpec).build();
        return javaFile.toString();
    }
}

The code generated after the annotation is finally used is as follows

package com.android.hdemo;

import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;

@Keep
public class MainActivityproxy {
  public MainActivityproxy(final MainActivity host) {
    host.setContentView(2131296284);
    host.jump=(android.widget.Button)host.findViewById(2131165257);
    host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.onClick(host.findViewById(2131165258));
      }
    });
    host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.onClick(host.findViewById(2131165257));
      }
    });
  }
}

5) Make comments effective

After we generate the code, we need to let the original class call the generated code

public class BindHelper {

    static final Map<Class<?>,Constructor<?>> Bindings = new HashMap<>();

    public static void inject(Activity activity){
        String classFullName = activity.getClass().getName() + ClassElementsInfo.classSuffix;
        try{
            Constructor constructor = Bindings.get(activity.getClass());
            if(constructor == null){
                Class proxy = Class.forName(classFullName);
                constructor = proxy.getDeclaredConstructor(activity.getClass());
                Bindings.put(activity.getClass(),constructor);
            }
            constructor.setAccessible(true);
            constructor.newInstance(activity);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2.3 debugging

First, add the following code to gradle.properties

android.enableSeparateAnnotationProcessing = true
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888

Then click Edit Configurations

Create a new remote

Then fill in the relevant parameters. 127.0.0.1 indicates the local machine, port is consistent with the one just filled in gradle.properties, and then click ok

Then adjust the Select Run/Debug Configuration option to the newly created Configuration, and click Build – Rebuild Project to start debugging.

2.4 use

The original class is shown below

@CompilerBindView(R.layout.first)
public class MainActivity extends AppCompatActivity {

    @CompilerBindView(R.id.jump)
    public Button jump;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        BindHelper.inject(this);
    }

    @CompilerBindClick({R.id.jump,R.id.jump2})
    public void onClick(View view){
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
}

The following are the generated classes

package com.android.hdemo;

import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;

@Keep
public class MainActivityproxy {
  public MainActivityproxy(final MainActivity host) {
    host.setContentView(2131296284);
    host.jump=(android.widget.Button)host.findViewById(2131165257);
    host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.onClick(host.findViewById(2131165258));
      }
    });
    host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.onClick(host.findViewById(2131165257));
      }
    });
  }
}

3. summary

The annotation framework looks very tall, but it's not difficult to understand. It's all a routine.

124 original articles published, 20 praised, 110000 visitors+
Private letter follow

Posted by dleone on Fri, 17 Jan 2020 21:03:12 -0800