preface
There are generally two purposes for defining annotations, the second of which is explained in the section following the introduction of this article.
-
The first kind: Android annotations using the enumeration of the introduction to define annotations, for data type and scope check, more work in the source code editing stage, is the IDE provides power functions.
-
The second kind: data injection and code generation, in the development process to reduce boilerplate code, optimize readability, help improve work efficiency and so on.
Structure of the annotation library
An annotation tool, generally divided into two modules: annotation definition module and annotation processor module. Take ButterKnife as an example:
- Add annotations to define modules
implementation 'com. Jakewharton: butterknife: 8.4.0'
Copy the code
- Add the annotation handler module
Annotation processors are typically added using annotationProcessor or kapt.
kapt 'com. Jakewharton: butterknife: 8.4.0' // Support kotlin source code
annotationProcessor 'com. Jakewharton: butterknife: 8.4.0' // Support Java source code
Copy the code
Custom annotation implementation
The annotation example is used to implement data injection.
Annotations to define
All annotations default inherited from Java. Lang, the annotation. The annotation. Type constraints for Annotation members: 8 basic data types, String, Class, Annotation and subclass, enumeration.
When defining an annotation, you can declare 0.. N members, as defined below, can be specified by default for members; Member names can be set according to the programming language’s variable naming rules.
// Target Specifies the scope of the StringIntentKey.
@Target(ElementType.FIELD)
// If you use dynamic annotations, you need to specify Retention as RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface StringIntentKey {
String value(a) default "";
}
Copy the code
Annotations to call
Once the annotation definition is introduced, it is ready to be called. You need to specify the parameter name. If the parameter name is value and there is only one member, you can omit it.
@ StringAnnotation (value = “data”),
@StringIntentKey("dynamic_data")
String dynamicData;
@StringIntentKey("static_data")
String staticData;
Copy the code
Annotation processing
For annotations to really work for data injection and code generation, it is the annotation processor that identifies and processes the annotation location. Annotation processors come in two types: dynamic and static.
- Dynamic annotation processing, the use of reflection technology, by scanning member variables, methods, etc., to identify the annotation call after the assignment operation. By dynamic, I mean that operations scan code dynamically at run time, so there is a performance cost.
public class DynamicUtil {
public static void inject(Activity activity) {
Intent intent = activity.getIntent();
/ / reflection
for (Field field : activity.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(StringIntentKey.class)) {
// Get the annotation
StringIntentKey annotation = field.getAnnotation(StringIntentKey.class);
String intentKey = annotation.value();
// Read the actual IntentExtra value
Serializable serializable = intent.getSerializableExtra(intentKey);
if (serializable == null) {
if (field.getType().isAssignableFrom(String.class)) {
serializable = ""; }}try {
/ / insert value
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(activity, serializable);
field.setAccessible(accessible);
} catch(IllegalAccessException e) { e.printStackTrace(); }}}}Copy the code
- Static annotation processing, as opposed to dynamic annotation processing. Annotation identification and processing is done at compile time without code scanning at runtime, thus saving runtime performance costs but increasing compile time.
Static annotation processing, which requires scanning source files at compile time to generate new source files, is explained separately in the following section code generation.
Code generation
Static annotations appear after and replace dynamic annotations. Static annotations, as opposed to dynamic annotations, place the interpretation of annotations at compile time and use the compiled results directly instead of the interpretation at run time. Therefore, the compilation phase needs to use the appropriate tools to generate the required code.
- Javapoet: A Java Library for generating Java code, see article.
- Auto-service:github.com/google/auto…
In this example, generate a Binder class for each annotated class and provide a unified entry for StaticMapper#bind for data injection. All you need to do is call this method at initialization.
public final class MainActivity$Binder {
public static final void bind(MainActivity activity) {
Intent intent = activity.getIntent();
if (intent.hasExtra("static_data")) {
activity.staticData = (String) intent.getSerializableExtra("static_data"); }}}Copy the code
public final class StaticMapper {
public static final void bind(Activity activity) {
if (activity instanceof MainActivity) {
MainActivity$Binder binder = new MainActivity$Binder();
binder.bind((com.campusboy.annotationtest.MainActivity) activity);
} else if (activity instanceof Main2Activity) {
Main2Activity$Binder binder = newMain2Activity$Binder(); binder.bind((com.campusboy.annotationtest.Main2Activity) activity); }}}Copy the code
- The annotation interpreter needs to inherit from the AbstractProcessor base class and declare this class to be an annotation Processor using @AutoService(processor.class).
import com.google.auto.service.AutoService;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {}Copy the code
public abstract class AbstractProcessor implements Processor {}Copy the code
- AbstractProcessor base class AbstractProcessor implements the Processor interface, where init() and getSupportedOptions() are implemented in AbstractProcessor. The main function of StaticIntentProcessor is to implement the process() method to generate the class.
public interface Processor {
Set<String> getSupportedOptions(a);
// A collection of class names for supported annotation classes
Set<String> getSupportedAnnotationTypes(a);
// The supported Java version
SourceVersion getSupportedSourceVersion(a);
void init(ProcessingEnvironment var1);
boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);
Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}
Copy the code
- Here is the static annotation processing in this example, identifying the annotation call and generating the corresponding code file. No longer need to be obtained by reflection at runtime.
@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {
private TypeName activityClassName = ClassName.get("android.app"."Activity").withoutAnnotations();
private TypeName intentClassName = ClassName.get("android.content"."Intent").withoutAnnotations();
@Override
public SourceVersion getSupportedSourceVersion(a) {
/ / support java1.7
return SourceVersion.RELEASE_7;
}
@Override
public Set<String> getSupportedAnnotationTypes(a) {
// Only the StringIntentKey annotation is handled
return Collections.singleton(StringIntentKey.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
// StaticMapper's bind method
MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addParameter(activityClassName, "activity");
// Find all classes that need to be injected
List<InjectDesc> injectDescs = findInjectDesc(set, re);
for (int i1 = 0; i1 < injectDescs.size(); i1++) {
InjectDesc injectDesc = injectDescs.get(i1);
// Create a Java file for the class to be annotated, as described above in IntentActivity$Binder
TypeName injectedType = createInjectClassFile(injectDesc);
TypeName activityName = typeName(injectDesc.activityName);
// $T import type
// Generate code for binding distribution
method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName);
method.addCode("\t$T binder = new $T(); \n", injectedType, injectedType);
method.addCode("\tbinder.bind((" + activityName + ") activity); \n", activityName, activityName);
method.addCode("}");
}
// Create the StaticMapper class
createJavaFile("com.campusboy.annotationtest"."StaticMapper", method.build());
return false;
}
private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {
Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();
// Get all elements marked by StringIntentKey
Set<? extends Element> elements = re.getElementsAnnotatedWith(StringIntentKey.class);
for (Element element : elements) {
// Only care about elements whose categories are attributes
if(element.getKind() ! = ElementKind.FIELD) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"only support field");
continue;
}
// The description type of the class is found here
// Because our StringIntentKey's annotation description is Field, the closingElement element is class
TypeElement classType = (TypeElement) element.getEnclosingElement();
System.out.println(classType);
// Cache the class to avoid duplication
List<String[]> nameList = targetClassMap.get(classType);
if (nameList == null) {
nameList = new ArrayList<>();
targetClassMap.put(classType, nameList);
}
// Annotated value, such as staticName
String fieldName = element.getSimpleName().toString();
// The type of the annotated value, e.g. String, int
String fieldTypeName = element.asType().toString();
// The value of the annotation itself, such as key_name
String intentName = element.getAnnotation(StringIntentKey.class).value();
String[] names = new String[]{fieldName, fieldTypeName, intentName};
nameList.add(names);
}
List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size());
for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) {
String className = entry.getKey().getQualifiedName().toString();
System.out.println(className);
// Encapsulate to a custom descriptor
InjectDesc injectDesc = new InjectDesc();
injectDesc.activityName = className;
List<String[]> value = entry.getValue();
injectDesc.fieldNames = new String[value.size()];
injectDesc.fieldTypeNames = new String[value.size()];
injectDesc.intentNames = new String[value.size()];
for (int i = 0; i < value.size(); i++) {
String[] names = value.get(i);
injectDesc.fieldNames[i] = names[0];
injectDesc.fieldTypeNames[i] = names[1];
injectDesc.intentNames[i] = names[2];
}
injectDescList.add(injectDesc);
}
return injectDescList;
}
private void createJavaFile(String pkg, String classShortName, MethodSpec... method) {
TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
for (MethodSpec spec : method) {
builder.addMethod(spec);
}
TypeSpec clazzType = builder.build();
try {
JavaFile javaFile = JavaFile.builder(pkg, clazzType)
.addFileComment(" This codes are generated automatically. Do not modify!")
.indent("")
.build();
// write to file
javaFile.writeTo(processingEnv.getFiler());
} catch(IOException e) { e.printStackTrace(); }}private TypeName createInjectClassFile(InjectDesc injectDesc) {
ClassName activityName = className(injectDesc.activityName);
ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder");
MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addParameter(activityName, "activity");
$T import as class, $N import as pure value, $S import as string
method.addStatement("$T intent = activity.getIntent()", intentClassName);
for (int i = 0; i < injectDesc.fieldNames.length; i++) {
TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]);
method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]);
method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S); \n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]);
method.addCode("}\n");
}
// Generate the final XXX$Binder file
createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build());
return injectedClass;
}
private TypeName typeName(String className) {
return className(className).withoutAnnotations();
}
private ClassName className(String className) {
// Base type descriptor
if (className.indexOf(".") < =0) {
switch (className) {
case "byte":
return ClassName.get("java.lang"."Byte");
case "short":
return ClassName.get("java.lang"."Short");
case "int":
return ClassName.get("java.lang"."Integer");
case "long":
return ClassName.get("java.lang"."Long");
case "float":
return ClassName.get("java.lang"."Float");
case "double":
return ClassName.get("java.lang"."Double");
case "boolean":
return ClassName.get("java.lang"."Boolean");
case "char":
return ClassName.get("java.lang"."Character");
default:}}// Manually parse java.lang.String into java.lang package name and String class name
String packageD = className.substring(0, className.lastIndexOf('. '));
String name = className.substring(className.lastIndexOf('. ') + 1);
return ClassName.get(packageD, name);
}
private static class InjectDesc {
private String activityName;
private String[] fieldNames;
private String[] fieldTypeNames;
private String[] intentNames;
@Override
public String toString(a) {
return "InjectDesc{" +
"activityName='" + activityName + '\' ' +
", fieldNames=" + Arrays.toString(fieldNames) +
", intentNames=" + Arrays.toString(intentNames) +
'} '; }}}Copy the code
Afterword.
In this example, a StaticMapper is generated in the APP module, but in a multi-module project, multiple StaticMapper class files will be generated, resulting in compilation exceptions and failure to complete the compilation process. This problem is left to be solved later.
The sample project
Example project: customize-Annotation
Code generation library: javaPoet uses this library to make code generation easier.