Annotations in Java are an amazing thing, and if you don’t, you can see custom annotations in the next hour. There are many Android libraries that use annotations, such as ButterKnife, and we’ll take a look. After learning about the annotation processor, we’ll try to write a simple ButterKnife to bind controls.
What is an annotation processor?
The Annotation Processor is a javAC tool that scans and compiles and processes annotations at compile time. You can define your own annotations and annotation handlers to get things done. An annotation processor that takes Java code or (compiled bytecode) as input and generates files (usually Java files). These generated Java files cannot be modified and will be compiled by JavAC just like their hand-written Java code. The annotated classes, variables, etc. are taken as input and processed by the annotation processor to generate the Java code you want to generate.
The processor AbstractProcessor
Processors are written with a set of conventions, inherited from AbstractProcessor. As follows:
public class MyProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } @Override public Set getSupportedAnnotationTypes() { return null; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { return true; }}Copy the code
- Init (ProcessingEnvironment processingEnv) is called by the annotation processing tool. The parameter ProcessingEnvironment provides tools such as Element, Filer, Messager, etc
- GetSupportedAnnotationTypes () specifies the annotation processor is registered to the an annotation, it is a collection of strings, means that you can support multiple types of annotations, and string is legal full name.
- GetSupportedSourceVersion specified Java version
- Process (Set Annotations, RoundEnvironment roundEnv) is also the primary, where you scan and process your annotations and generate Java code. The information is stored in the RoundEnvironment parameter, which I’ll discuss later.
It is also available in Java7
@ SupportedSourceVersion (SourceVersion latestSupported ()) @ SupportedAnnotationTypes ({/ / a collection of legal note full name})Copy the code
Instead of getSupportedSourceVersion () and getSupportedAnnotationType (), no problem, can also use annotations in the annotation processing from the device.
Register annotation handlers
Packaging annotation Processor. When you need a special file javax.mail annotation. Processing. The Processor in the meta-inf/services directory
-- myprcessor. Jar - com -- -- -- -- -- - example -- -- -- -- -- -- -- -- MyProcessor. Class -- meta-inf -- -- -- -- -- - services -- -- -- -- -- -- -- -- javax.mail. Annotation. Processing. The ProcessorCopy the code
Packed into javax.mail. The annotation. Processing. The contents of the Processor is the legal name of the Processor, line breaks between multiple processors.
com.example.myprocess.MyProcessorA
com.example.myprocess.MyProcessorBCopy the code
Google provides a library to register processors
The compile 'com. Google. Auto. Services: auto - service: 1.0 rc2'Copy the code
A note is done:
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
...
}Copy the code
We’ve read all about ButterKnife here
1. Custom annotations
2. Parse annotations with an annotation processor
3. Generate a Java file after the parsing is complete
BufferKnife use:
public class MainActivity extends AppCompatActivity { @Bind(R.id.rxjava_demo) Button mRxJavaDemo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); mRxJavaDemo.setText("Text"); }}Copy the code
Then we compile, open path: / app/build/intermediates/classes/release/com/Ming/rxdemo/MainActivity? ViewBinder.class
This is the Java file we generated, and you can see that Button is already initialized in Bind.
public class MainActivity$ViewBinder implements ViewBinder { public MainActivity$ViewBinder() { } public void bind(Finder finder, T target, Object source) { View view = (View)finder.findRequiredView(source, 2131492944, "field \'mRxJavaDemo\'"); target.mRxJavaDemo = (Button)finder.castView(view, 2131492944, "field \'mRxJavaDemo\'"); } public void unbind(T target) { target.mRxJavaDemo = null; }}Copy the code
Next we create a project that writes a simple example of binding a control with annotations
The project structure
--apt-demo ---- bindView-Annotation (Java Library) ---- BindView-API (Android Library) ---- bindview-Compiler (Java Library) - app (Android app)Copy the code
- Bindview-annotation Annotation declaration
- Bindview-api calls the Android SDK API
- Bindview-compiler annotations are processor-specific
- The app test app
1. Create an @bindView annotation under bindView-Annotation that returns a value, an integer named value, representing the control ID.
@target (ElementType.FIELD) @Retention(retentionPolicy.class) public @interface BindView {/** * @return */ int value(); }Copy the code
2. Create the annotation processor BindViewProcessor in bindview-Compiler and register it to do basic initialization.
@autoService (processor.class) public class BindViewProcessor extends AbstractProcessor {/** * private Filer mFiler; /** * private Elements mElementUtils; /** * private Messager mMessager; Private Map mAnnotatedClassMap = new HashMap<>(); @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); mFiler = processingEnv.getFiler(); } @Override public Set getSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); // The annotation handler supports the annotation set. } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { return true; }}Copy the code
AnnotatedClass, AnnotatedClass, AnnotatedClass, AnnotatedClass In this case, @bindView is used to mark the members of a class. A class can have multiple members, like an Activity can have multiple controls, or a container can have multiple controls, etc. As follows:
package com.mingwei.myprocess.model; import com.mingwei.myprocess.TypeUtil; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; /** * Created by mingwei on 12/10/16. * CSDN: http://blog.csdn.net/u013045971 * Github: https://github.com/gumingwei * / public class AnnotatedClass {/ class name is * * * * / public TypeElement mClassElement; /** * List of member variables */ public List mFiled; /** * public Elements mElementUtils; public AnnotatedClass(TypeElement classElement, Elements elementUtils) { this.mClassElement = classElement; this.mElementUtils = elementUtils; this.mFiled = new ArrayList<>(); } / * * * * get the current class full name/public String getFullClassName () {return mClassElement. GetQualifiedName (). The toString (); Public void addField(BindViewField field) {filed. Add (field); } /** * public JavaFile generateFinder() {return null; } /** * Package name */ public String getPackageName(TypeElement Type) {return mElementUtils.getPackageOf(type).getQualifiedName().toString(); } /** * class name */ private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; return type.getQualifiedName().toString().substring(packageLen).replace('.', 'Copy the code
); }} members are represented by BindViewField, no complicated logic, in the constructor to determine the type and initialization, simple get function
package com.mingwei.myprocess.model; import com.mingwe.myanno.BindView; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; /** * Created by mingwei on 12/10/16. * CSDN: http://blog.csdn.net/u013045971 * Github: https://github.com/gumingwei * be BindView annotations of the tag field model class * / public class BindViewField {private VariableElement mFieldElement; private int mResId; public BindViewField(Element element) throws IllegalArgumentException { if (element.getKind() ! = elementkind. FIELD) {throw new IllegalArgumentException(string. format("Only FIELD can be annotated with @%s", BindView.class.getSimpleName())); } mFieldElement = (VariableElement) element; / / get annotations and value BindView BindView = mFieldElement. GetAnnotation (BindView. Class); mResId = bindView.value(); if (mResId < 0) { throw new IllegalArgumentException(String.format("value() in %s for field % is not valid", BindView.class.getSimpleName(), mFieldElement.getSimpleName())); } } public Name getFieldName() { return mFieldElement.getSimpleName(); } public int getResId() { return mResId; } public TypeMirror getFieldType() { return mFieldElement.asType(); }}Copy the code
You see a lot of elements here, and there are elements in Xml parsing. There is also the concept of Element in Java source files:
package com.example; // PackageElement
public class MyClass { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {
}
}Copy the code
The next step is to parse the annotations in the processor’s process
Flush before each parse, because the process method may go more than once.
Take the annotation model and iterate through the call to generate Java code
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnv);
} catch (IllegalArgumentException e) {
error(e.getMessage());
return true;
}
try {
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
info("generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
error("Generate file failed,reason:%s", e.getMessage());
}
return true;
}Copy the code
ProcessBindView and getAnnotatedClass
RoundEnviroment * @param roundEnv */ private void processBindView(RoundEnvironment roundEnv) {for (Element) element : roundEnv.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field); }} /** * * @element */ private AnnotatedClass getAnnotatedClass(Element Element) {TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); String fullClassName = encloseElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(encloseElement, mElementUtils); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; }Copy the code
3. Before generating Java, we need to create some classes in BindView-API to be used with bindView-Compiler.
When using Butterknife, you need to drop bindview.bind (this) in onCreate. Bindview.bind (this) can be interpreted as calling your generated Java code. If the generated Java code is not associated with the place you want to use it, it will be useless. After some control initialization is done in the generated code, your control becomes available.
Interface: Finder defines findView methods
Implementation class: ActivityFinder Activity used, ViewFinder View used
Interface: the Injector method will be created in the generated Java file to initialize the control with the parameters passed in this method.
Helper class: ViewInjector calls and passes arguments
This code I will not paste, a little bit of content, a look will understand.
4. Generate Java code in AnnotatedClass
The generated code uses a nice library, Javapoet. Classes, methods, can all be created using the builder, so it’s easy to get started, no more concatenating strings. Hahaha ~
Public JavaFile generateFinder() {injectMethodBuilder = injectMethodBuilder ("inject") .addModifiers(modification.public)// Add description. AddAnnotation (override.class)// Add annotations .addparameter (TypeName. Get (mClassElement. AsType ()), "host", Modifier.FINAL)// addParameter.addparameter (TypeName. AddParameter (TypeUtil.FINDER, "FINDER "); // Add parameter for (BindViewField field: MFiled) {/ / add a line injectMethodBuilder. AddStatement (" host. $N = ($T) finder. FindView (source, $L) ", the field. The getFieldName (), ClassName.get(field.getFieldType()), field.getResId()); } String packageName = getPackageName(mClassElement); String className = getClassName(mClassElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); // Build the class TypeSpec finderClass = TypeSpec. ClassBuilder (bindClassname.simplename () + "$Injector")// Class name AddModifiers (Modifier. PUBLIC) / / add a description. AddSuperinterface (ParameterizedTypeName. Get (TypeUtil INJECTOR, TypeName. Get (mClassElement asType ()))) / / add an interface (class/interface, paradigm). AddMethod (injectMethodBuilder. The build ()) / / add methods. The build (); return JavaFile.builder(packageName, finderClass).build(); } public String getPackageName(TypeElement type) { return mElementUtils.getPackageOf(type).getQualifiedName().toString(); } private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; return type.getQualifiedName().toString().substring(packageLen).replace('.', 'Copy the code
); }
You can debug annotation handler code in system.out code.
Another thing to be aware of is cross-references between projects.
Bindview – complier bindview reference – the annotation
The app refers to the remaining three modules as apt when referring to bindview-complier
apt project(':bindview-compiler')Copy the code
I’ll leave you there. The Demo is on Github