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
- http://blog.csdn.net/industriously/article/details/53932425
- http://blog.csdn.net/lmj623565791/article/details/43452969
- https://github.com/square/javapoet