Android Compile-Time Annotations Generate Code

Keywords: Java Android Gradle ButterKnife

Android Compile-Time Annotations Generate Code

*This project is for learning purposes only and ButterKnife is recommended as part of the project*

1 Introduction

_In the current stage of Android development, annotations are becoming more and more popular, such as ButterKnife, Retrofit, Dragger, EventBus, and so on.Depending on the processing time, there are two types of annotations, one is run-time annotations and the other is compile-time annotations, which have been criticized by some people for their performance problems.The core of compile-time annotations relies on APT(Annotation Processing Tools) implementations, which add annotations to certain code elements (such as types, functions, fields, etc.). At compile-time, the compiler checks the subclasses of AbstractProcessor, calls the process function of that type, and passes all the elements added annotations to the process function so that developers canProcessing is done in the compiler, for example, generating new Java classes from annotations, which is the basic principle of open source libraries such as EventBus, Retrofit, Dragger, and so on.
The Java API already provides a framework for scanning source code and parsing annotations, and you can inherit the AbstractProcessor class to provide your own parsing annotation logic.Below we will learn how to generate java files from compile-time annotations in android Studio.[From: http://blog.csdn.net/industriously/article/details/53932425]

_This time, we intend to do a framework similar to butterKnife, but due to time constraints, we will only do the binding of view for a while, roughly as follows

package cn.yzl.abstractprocessor;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

import cn.yzl.viewinject.ViewInjectHelper;
import cn.yzl.viewinject.annotation.ViewInject;

public class MainActivity extends AppCompatActivity {


    @ViewInject(R.id.myview)
    TextView myView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //Use similar to butterKnifer
        ViewInjectHelper.inject(this);

        myView.setText("Assignment succeeded");

    }
}

Generated Classes

package cn.yzl.abstractprocessor;

import android.view.View;
import android.widget.TextView;
import cn.yzl.abstractprocessor.MainActivity;
import cn.yzl.viewinject.Utils;

class MainActivity_ViewInject {
    public MainActivity_ViewInject(MainActivity target, View rootView) {
        target.myView = (TextView)Utils.getViewById(rootView, 2131427422);
    }
}

Why write the generated class like this instead of a class that contains static methods? This is to handle the onclick event, and I want to be able to cache this class. I don't need to load this class with classloader every time. In fact, it's unreasonable to use the construction method here. However, I'm lazy....

2 Project Structure

  • app our demo
  • viewinject-annotation annotation annotation package, java-library module
  • viewinject-complie compile processing package, java-library module
  • The package that the viewinject project really needs to depend on, Android-library module, finds the classes we generated at compile time and executes them, and changes the package to depend on viewinject-annotation, so you only need to add this dependent package in the app

3 Custom Notes

package cn.yzl.viewinject.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * View binding annotations
 * Created by YZL on 2017/8/4.
 */
@Retention(RetentionPolicy.CLASS)//
@Target(ElementType.FIELD)
public @interface ViewInject {
    int value();
}

This comment is simple, acts on variables, note that Retention here is no longer the old Runtime, but CLASS, meaning that this comment is left at compile time only and not at run time

4 Compile-time processor

Take a look at its dependency libraries first

    //A library for generating code that is much more convenient, powerful, and automatically guided than handwriting
    compile 'com.squareup:javapoet:1.9.0'
    //google's auto service library, automatically generating services, eliminating manual configuration of resources/META-INF/services
    compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
    //Depends on the annotation item because you want to read the annotation here
    compile project(':viewinject-annotation')
  • ViewInjectProcess
package cn.yzl.process.library;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import cn.yzl.viewinject.annotation.ViewInject;
import cn.yzl.viewinject.annotation.ViewOnClick;

@AutoService(Processor.class)
@SupportedAnnotationTypes("cn.yzl.viewinject.annotation.ViewInject")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ViewInjectProcess extends AbstractProcessor {

    //Suffix name of generated class
    public static final String SUFFIX = "_ViewInject";

    //Gets the View class for javapoet code generation
    ClassName VIEW = ClassName.get("android.view", "View");

    //Getting the Utils class, findviewbyid, is a method
    ClassName UTILS = ClassName.get("cn.yzl.viewinject", "Utils");

    //Print message class
    private Messager messager;

    //The resulting file is written in through this
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {

        List<TypeBean> types = findAnnotations(roundEnv);

        if (types.size() == 0) {
            return false;
        } else {
            generateJavaFile(types);
        }

        return false;
    }


    /**
     * Find the commented variable and its class, encapsulate it as a typeBean, and store it in the list
     * @param roundEnv
     * @return
     */
    public List<TypeBean> findAnnotations(RoundEnvironment roundEnv) {
        List<TypeBean> types = new ArrayList<>();

        Set<? extends Element> eFilds = roundEnv
                .getElementsAnnotatedWith(ViewInject.class);
        Set<? extends Element> eMethods = roundEnv
                .getElementsAnnotatedWith(ViewOnClick.class);
        for (Element e : eFilds) {
            TypeElement typeElement = (TypeElement) e.getEnclosingElement();
            String qualifiedName = typeElement.getQualifiedName().toString();
            TypeBean typeBean = getTypeBeanByName(types, typeElement, qualifiedName);

            typeBean.addField((VariableElement) e);
        }

        for (Element e : eMethods) {
            TypeElement typeElement = (TypeElement) e.getEnclosingElement();
            String qualifiedName = typeElement.getQualifiedName().toString();
            TypeBean typeBean = getTypeBeanByName(types, typeElement, qualifiedName);
            if (typeBean.onClickmethod != null) {
//                error(e,msg);
            } else {
                typeBean.onClickmethod = (ExecutableElement) e;
            }
        }
        return types;
    }


    /**
     * Generate auxiliary classes from resolved classes
     * @param types
     */
    private void generateJavaFile(List<TypeBean> types) {
        for (TypeBean typebean : types) {
            generateJavaFileForOneType(typebean);
        }
    }

    /**
     * Generating auxiliary classes
     * @param typebean
     */
    private void generateJavaFileForOneType(TypeBean typebean) {

        CodeBlock.Builder codeBuilder = CodeBlock.builder();

        //Write findviewbyid code
        for (int i = 0; i < typebean.fields.size(); i++) {
            //findViewById
            VariableElement variableElement = typebean.fields.get(i);
            ViewInject annotation = variableElement.getAnnotation(ViewInject.class);
            codeBuilder.add(
                    "target." + variableElement.getSimpleName().toString() + "=" +
                            "$T.getViewById(rootView," + annotation.value() + ");\n"

                    , UTILS);
        }

        //TODO onclick

        // Generation Construction Method
        MethodSpec method = MethodSpec
                .constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(typebean.typeElement.asType()), "target")
                .addParameter(VIEW, "rootView")
                .addCode(codeBuilder.build())
                .build();

        //Generate Class File
        TypeSpec typeSpec = TypeSpec.classBuilder(typebean.simpleName + SUFFIX)
                .addMethod(method)
                .build();
        writeJavaFile(typebean.packName, typeSpec);
    }

    /**
     * @param types
     * @param typeElement
     * @param className   @return
     */
    public TypeBean getTypeBeanByName(List<TypeBean> types, TypeElement typeElement, String className) {
        for (int i = 0; i < types.size(); i++) {
            if (types.get(i).equalsClass(className))
                return types.get(i);
        }
        TypeBean typeBean = new TypeBean(typeElement, className);
        types.add(typeBean);
        return typeBean;
    }

    /**
     * Write Class
     * @param packName
     * @param typeSpec
     */
    public void writeJavaFile(String packName, TypeSpec typeSpec) {
        JavaFile javaFile = JavaFile.builder(packName, typeSpec).build();
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
        }
    }
}

There is a class, TypeBean, which contains all the methods annotated by Viewinject in a stored class, and the package name and full name of the class, which is no longer parsed here

Use

In gradle of app

dependencies {
    compile project(':ViewInject')
    annotationProcessor project(':viewinject-complie')
}

That way, the auxiliary classes we need will be generated when compiling

There is a problem that Chinese cannot be used in the processor, including comments, otherwise the following error will be reported

5 Processing Auxiliary Classes

_This class is very simple: get the class we generated, get the method we generated, drop it and ok

    package cn.yzl.viewinject;

import android.app.Activity;
import android.util.Log;
import android.view.View;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Created by YZL on 2017/8/4.
 */

public class ViewInjectHelper {
    public static final String SUFFIX = "_ViewInject";

    public static void inject(Activity target) {
        ClassLoader classLoader = target.getClass().getClassLoader();
        try {
            //Get the full name of the generated class
            String proxyClazzName = target.getClass().getName() + SUFFIX;
            Log.e("ViewInjectHelper#inject", proxyClazzName);
            //Load Class
            Class<?> aClass = classLoader.loadClass(proxyClazzName);

            //Get docView
            View rootView = target.getWindow().getDecorView();

            //Get the declared constructor, here's one
            Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
            //Cancel security check
            declaredConstructors[0].setAccessible(true);
            //Execute construction method
            declaredConstructors[0].newInstance(target, rootView);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Okay, so we can use it

6 Debug the compilation process

  • Add the following two lines of code to the gradle.properties file and synchronize the gradle file
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
  • Edit Run Options

-Add remote

  • Just use the default configuration

  • Select the remote we configured, click the debug button, and if successful, the link will be successful.

  • Break in our ViewInjectProcessor, then rebuild the project and debug

7 Supplementary Code Reference

    //Another way to generate code
    JavaFileObject source = processingEnvironment.getFiler().createSourceFile("cn.yzl.library.generated.GeneratedClass");
    Writer writer = source.openWriter();
    writer.write("The code you generated");
    writer.flush();
    writer.close();

8 Reference

9 Source Address

https://github.com/yizeliang/ViewInject

Posted by ElizaLeahy on Fri, 07 Jun 2019 11:11:52 -0700