PS: Humans are creatures that are very receptive to self-suggestion. If you give yourself negative suggestions, you can easily become despondent. If you give yourself positive suggestions, you will also become more positive.
Today, let's take a look at the knowledge related to compile-time annotations. I believe that after manual practice, you will find it easier to understand frameworks like Dagger, ARouter, and ButterKnife that use compile-time annotations, as well as their internal source code implementations. The content is as follows:
- Compile-time and runtime annotations
- Annotation Processing Tool (APT)
- AbstractProcessor
- Element and Elements
- Custom annotation processors
- Using custom annotation processors
Compile-time and Runtime Annotations#
First, let's understand the difference between compile-time and runtime:
- Compile-time: This refers to the process where the compiler translates source code into machine-readable code. In Java, it means compiling Java source code into bytecode files recognized by the JVM.
- Runtime: This refers to the process where the JVM allocates memory and interprets the bytecode files.
The meta-annotation
@Retention
determines whether the annotation is available at compile-time or runtime, with the following configurable strategies:public enum RetentionPolicy { SOURCE, // Discarded at compile-time, exists only in source code CLASS, // Default strategy, discarded at runtime, exists only in class files RUNTIME // Annotation information is recorded in class files at compile-time, retained at runtime, can be accessed via reflection }
Besides the differences mentioned above, compile-time and runtime annotations are implemented differently. Compile-time annotations are generally implemented through annotation processors (APT), while runtime annotations are typically implemented through reflection.
For more information on annotations and reflection, you can refer to the following two articles:
What is APT#
APT (Annotation Processing Tool) is a tool provided by javac that can process annotations, used to scan and process annotations at compile-time. In simple terms, APT allows you to obtain information about annotations and their locations, which can be used to generate code during compilation. Compile-time annotations are generated through APT using annotation information to fulfill certain functions, with typical examples being ButterKnife, Dagger, and ARouter.
AbstractProcessor#
AbstractProcessor
implementsProcessor
and is the abstract class for annotation processors. To implement an annotation processor, you need to extendAbstractProcessor
. The main method descriptions are as follows:
- init: Initializes the Processor, from which you can obtain utility classes
Elements
,Types
,Filer
, andMessager
from theProcessingEnvironment
parameter;- getSupportedSourceVersion: Returns the Java version being used;
- getSupportedAnnotationTypes: Returns the names of all annotations to be processed;
- process: Obtains all specified annotations for processing;
The process method may be executed multiple times during runtime until no other classes are generated.
Element and Elements#
Element
is similar to a tag in XML. In Java,Element
represents program elements such as classes, members, methods, etc. EachElement
represents a specific structure. For operations onElement
objects, please use visitor or getKind() methods for judgment and specific processing. The subclasses ofElement
are as follows:
- ExecutableElement
- PackageElement
- Parameterizable
- QualifiedNameable
- TypeElement
- TypeParameterElement
- VariableElement
The above element structures correspond to the following code structure:
// PackageElement package manu.com.compiler; // TypeElement public class ElementSample { // VariableElement private int a; // VariableElement private Object other; // ExecutableElement public ElementSample() { } // Method parameter VariableElement public void setA(int newA) { } // TypeParameterElement represents parameterized types used in generic parameters }
Elements
is a utility class for processingElement
, providing only interfaces, with specific implementations in the Java platform.Custom Compile-time Annotation Processor#
Below is an implementation of an annotation
@Bind
using APT to mimic ButterKnife's annotation@BindView
. The project structure is as follows:
In the API module, define the annotation
@Bind
as follows:@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface Bind { int value(); }
The compiler module is a Java module that introduces Google's auto-service to generate relevant files under META-INFO. Javapoet is used for easier creation of Java files. The custom annotation processor
BindProcessor
is as follows:// Used to generate META-INF/services/javax.annotation.processing.Processor file annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7' // Used for creating Java files implementation 'com.squareup:javapoet:1.12.1'
/** * BindProcessor */ @AutoService(Processor.class) public class BindProcessor extends AbstractProcessor { private Elements mElements; private Filer mFiler; private Messager mMessager; // Stores the corresponding BindModel for a certain class private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>(); @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mMessager = processingEnvironment.getMessager(); print("init"); // Initialize Processor mElements = processingEnvironment.getElementUtils(); mFiler = processingEnvironment.getFiler(); } @Override public SourceVersion getSupportedSourceVersion() { print("getSupportedSourceVersion"); // Return the Java version being used return SourceVersion.RELEASE_8; } @Override public Set<String> getSupportedAnnotationTypes() { print("getSupportedAnnotationTypes"); // Return the names of all annotations to be processed Set<String> set = new HashSet<>(); set.add(Bind.class.getCanonicalName()); set.add(OnClick.class.getCanonicalName()); return set; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { print("process"); mTypeElementMap.clear(); // Get Element of specified Class type Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class); // Iterate and store the matching Elements for (Element element : elements) { // Get the fully qualified class name of the Element TypeElement typeElement = (TypeElement) element.getEnclosingElement(); print("process typeElement name:" + typeElement.getSimpleName()); List<BindModel> modelList = mTypeElementMap.get(typeElement); if (modelList == null) { modelList = new ArrayList<>(); } modelList.add(new BindModel(element)); mTypeElementMap.put(typeElement, modelList); } print("process mTypeElementMap size:" + mTypeElementMap.size()); // Java file generation mTypeElementMap.forEach((typeElement, bindModels) -> { print("process bindModels size:" + bindModels.size()); // Get package name String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString(); // Generate the file name for the Java file String className = typeElement.getSimpleName().toString(); String newClassName = className + "_ViewBind"; // MethodSpec MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(ClassName.bestGuess(className), "target"); bindModels.forEach(model -> { constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)", model.getViewFieldName(), model.getViewFieldType(), model.getResId()); }); // typeSpec TypeSpec typeSpec = TypeSpec.classBuilder(newClassName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(constructorBuilder.build()) .build(); // JavaFile JavaFile javaFile = JavaFile.builder(packageName, typeSpec) .addFileComment("AUTO Create") .build(); try { javaFile.writeTo(mFiler); } catch (IOException e) { e.printStackTrace(); } }); return true; } private void print(String message) { if (mMessager == null) return; mMessager.printMessage(Diagnostic.Kind.NOTE, message); } }
The
BindModel
is a simple encapsulation of the annotation@Bind
information, as follows:/** * BindModel */ public class BindModel { // Member variable Element private VariableElement mViewFieldElement; // Member variable type private TypeMirror mViewFieldType; // View resource Id private int mResId; public BindModel(Element element) { // Validate if the Element is a member variable if (element.getKind() != ElementKind.FIELD) { throw new IllegalArgumentException("element is not FIELD"); } // Member variable Element mViewFieldElement = (VariableElement) element; // Member variable type mViewFieldType = element.asType(); // Get the value of the annotation Bind bind = mViewFieldElement.getAnnotation(Bind.class); mResId = bind.value(); } public int getResId(){ return mResId; } public String getViewFieldName(){ return mViewFieldElement.getSimpleName().toString(); } public TypeMirror getViewFieldType(){ return mViewFieldType; } }
In the bind module, create the file to be generated:
/** * Initialization */ public class BindKnife { public static void bind(Activity activity) { // Get the fully qualified class name of the activity String name = activity.getClass().getName(); try { // Reflectively create and inject Activity Class<?> clazz = Class.forName(name + "_ViewBind"); clazz.getConstructor(activity.getClass()).newInstance(activity); } catch (Exception e) { e.printStackTrace(); } } }
Of course, ButterKnife should cache the created objects, so it doesn't create new objects every time. Although it also uses reflection, the testing of reflection is significantly reduced compared to runtime annotations, so its performance is better than that of runtime annotations. This is also a distinction between compile-time and runtime annotations.
Using Custom Annotation Processors#
The usage is similar to ButterKnife, as follows:
public class MainActivity extends AppCompatActivity{ @Bind(R.id.tvData) TextView tvData; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); BindKnife.bind(this); tvData.setText("data"); } }
Understanding compile-time annotations may not lead you to immediately reinvent the wheel, but it is very helpful when looking at other frameworks and provides another avenue for solving problems.