Introduction
Java annotations, also known as Java annotations, are special syntax metadata that can be added to source code in Java 5.0.
Classes, methods, variables, parameters, and packages in Java can be labeled. Java annotation is different from Javadoc, and annotation is self-explanatory. When the compiler generates a class file, the annotation can be embedded into the bytecode and obtained during execution by the Java virtual machine.
According to the different values specified by the meta annotation @ Retention, the annotation can be divided into three types: SOURCE, CLASS, and RUNTIME. When declared as SOURCE, the annotation is reserved only at the SOURCE code level and discarded during compilation. When declared as CLASS, the annotation is recorded in the class file by the compiler, however, it is ignored during RUNTIME. The default Retention level is CLASS. When declared as RUNTIME, the annotation will be retained to RUNTIME and can be obtained through reflection at RUNTIME.
Next we will introduce the method for processing annotation at the CLASS level during the compilation period.
APT
Annotation Processing Tool is a built-in Tool for javac to scan and process Annotation information during compilation. From JDK 6, apt exposes available APIs. A specific processor receives a Java source code or compiled bytecode as the input, and then outputs some files (usually. java files ). This means that you can use apt to dynamically generate code logic. Note that apt can only generate new Java classes and cannot modify existing Java classes. All generated Java classes will be compiled with other source code by javac.
Define and use annotations
For example, here we define an annotation Meta for Field annotation, which contains two parameters, repeat and id. In the compilation phase, we will assign values to the labeled Field by processing this annotation, for example, if repeat is 2 and id is Aa, the labeled Field is assigned "AaAa ".
@ Retention (RetentionPolicy. CLASS)
@ Target (ElementType. FIELD)
Public @ interface Meta {
Int repeat () default 0;
String id () default "";
}
Use annotation on Field
@ Meta (repeat = 3, id = "pai_^ ")
Public String test;
Process annotation
Next we will write a processor to process the Meta annotation defined above based on Android Studio.
Create a Module
The annotation parser is developed as a module in the Android Project. Create a Module and select Java Library as the type.
Create a processor
Annotations must be processed by annotation processors. All annotation processors implement the Processor interface. Generally, we choose to inherit AbstractProcessor to create a custom annotation Processor.
Inherits AbstractProcessor and implements public boolean process (Set <? Extends TypeElement> annotations, RoundEnvironment roundEnv) method. In the method parameters, annotations contains annotations supported by the processor declaration and used in the source code. roundEnv contains the context environment for Annotation Processing. If this method returns true, it indicates that the annotation has been processed. If it returns false, it will be handed over to other processors for further processing.
Declare the supported annotation types and source code versions
Override the getSupportedSourceVersion method and return the source code version supported by the processor. Generally, return SourceVersion. latestSupported () directly.
Override the getSupportedAnnotationTypes method and return the annotation type to be processed by the processor. Here, a set containing all annotation fully qualified names must be returned.
In Java 7 and later versions, you can use class annotation @ SupportedAnnotationTypes and @ SupportedSourceVersion to replace the above method for declaration.
Declare annotation processor
The annotation processor needs to register with JVM before use, create a services directory under the module's META-INF directory, and create a directory named javax. annotation. processing. processor file, in which the annotation Processor is declared row by row. Similarly, what needs to be declared here is the fully qualified name of the processor class.
Another easy way is to use the auto-services Library provided by Google, in build. introduce com. google. auto. service: auto-service: 1.0-rc2, and add annotation @ AutoService (Processor. class), auto-services is also an annotation processor that generates a declaration file for this module during compilation.
Parse annotation
First, we define an interface to standardize the generated class:
Public interface Actor {
Void action ();
}
Define another class structure to describe the generated Java class:
Public class TargetGen <T extends Target> implements Actor {
Protected T target;
Public TargetGen (T obj ){
This.tar get = obj;
}
@ Override
Public void action (){
// Value assignment
}
}
If we have A class A, where Field f contains Meta annotations, we will generate an AGen class for it and complete the assignment of f in the action method.
Complete annotation parsing and code generation in the process method:
@ Override
Public boolean process (Set <? Extends TypeElement> annotations, RoundEnvironment roundEnv ){
/* RoundEnv. getRootElements () returns all classes in the project
In practical applications, each Class needs to be filtered first to improve efficiency and avoid scanning the content of each Class */
For (Element e: roundEnv. getRootElements ()){
List <String> statements = new ArrayList <> ();
/* Traverse all elements in the Class */
For (Element el: e. getEnclosedElements ()){
/* Only process fields that contain annotations and are modified to public */
If (el. getKind (). isField () & el. getAnnotation (Meta. class )! = Null & el. getModifiers (). contains (Modifier. PUBLIC )){
/* Obtain annotation information and generate code snippets */
Meta meta = el. getAnnotation (Meta. class );
Int repeat = meta. repeat ();
String seed = meta. id ();
String result = "";
For (int I = 0; I <repeat; I ++ ){
Result + = seed;
}
Statements. add ("\ t \ ttarget." + el. getSimpleName () + "= \" "+ result + "\";");
}
}
If (statements. size () = 0 ){
Return true;
}
String enclosingName;
If (e instanceof PackageElement ){
EnclosingName = (PackageElement) e). getQualifiedName (). toString ();
} Else {
EnclosingName = (TypeElement) e). getQualifiedName (). toString ();
}
/* Obtain the class name and package of the generated class */
String pkgName = enclosingName. substring (0, enclosingName. lastIndexOf ('.'));
String clsName = e. getSimpleName () + "Gen ";
Log (pkgName + "," + clsName );
/* Create a file and write the code content */
Try {
JavaFileObject f = processingEnv. getFiler (). createSourceFile (clsName );
Log (f. toUri (). toString ());
Writer writer = f. openWriter ();
PrintWriter printWriter = new PrintWriter (writer );
PrintWriter. println ("// Auto generated code, do not modify it! ");
PrintWriter. println ("package" + pkgName + ";");
PrintWriter. println ("\ nimport com. moxun. Actor; \ n ");
PrintWriter. println ("public class" + clsName + "<T extends" + e. getSimpleName () + "> implements Actor {");
PrintWriter. println ("\ tprotected T target ;");
PrintWriter. println ("\ n \ tpublic" + clsName + "(T obj ){");
PrintWriter. println ("\ t \ tthis.tar get = obj ;");
PrintWriter. println ("\ t} \ n ");
PrintWriter. println ("\ t @ Override ");
PrintWriter. println ("\ tpublic void action (){");
For (String statement: statements ){
PrintWriter. println (statement );
}
PrintWriter. println ("\ t }");
PrintWriter. println ("}");
PrintWriter. flush ();
PrintWriter. close ();
Writer. close ();
} Catch (IOException e1 ){
E1.printStackTrace ();
}
}
Return true;
}
Add the dependency of the processor module to the dependencies of the target module, clean and rebuild the project, the source code can be processed by a custom annotation processor and generated to the build/intermediates/classes directory. Because of the issue of the Android Gradle plug-in, the generated class will still be put in this directory until the plug-in version 2.2.0-alpha4. The source files under the intermediates directory are not indexed by IDE, which makes debugging of generated code inconvenient. However, this does not affect the subsequent compilation process. In future versions, this issue may be corrected, and the product will be output to the correct place, that is, the build/generated/source/apt directory.
Use the generated class at runtime
During runtime, reflection can be used to access the generated class. A simple help class is defined here to instantiate the generated class and assign values to the Target Field:
Public class MetaLoader {
Public static void load (Object obj ){
String fullName = obj. getClass (). getCanonicalName ();
String pkgName = fullName. substring (0, fullName. lastIndexOf ('.'));
String clsName = pkgName + "." + obj. getClass (). getSimpleName () + "Gen ";
Try {
Class <Actor> clazz = (Class <Actor>) Class. forName (clsName );
Constructor <Actor> constructor = clazz. getConstructor (obj. getClass ());
Actor actor = constructor. newInstance (obj );
Actor. action ();
} Catch (Exception e ){
E. printStackTrace ();
}
}
}
Call MetaLoader. load when the target class is initialized, and pass in the target class instance to complete the Field assignment operation.
Exclude processors during packaging
Because the auto-service Library is introduced earlier, the Duplicate files copied in apk META-INF/services/javax error will be returned when the APK is finally packaged. annotation. processing. processor, and this file is not required at runtime, so you can exclude this file in packagingOptions to avoid this error:
PackagingOptions {
Exclude 'meta-INF/services/javax. annotation. processing. Processor'
}
However, this is not a complete solution. As mentioned above, the annotation processor is completely useless at runtime. Can it be used only during compilation without being packaged into the final product? The answer is yes.
Add the plug-in build. gradle of the project:
Dependencies {
Classpath 'com. neenbedankt. gradle. plugins: android-apt: 8080'
// Etc ......
}
Apply the plug-in module build. gradle:
Apply plugin: 'com. neenbedankt. android-apt'
After the plug-in is applied, dependencies adds a new dependency method apt and modifies the dependency declaration:
Dependencies {
Compile fileTree (dir: 'libs', include: ['*. Jar'])
Apt project (': processor ')
// Etc ......
}
After such declaration, classes in the processor module will not be packaged into the final product, which is conducive to reducing the product volume.
Debug annotation processor
Add a new Run/Debug deployments in Android Studio and select Remote as the type;
Add
Org. gradle. jvmargs =-agentlib: jdwp = transport = dt_socket, server = y, suspend = n, address = 5005
Select the Configuration defined above, and click the Debug button to wait for the target process to attach;
Set breakpoints in the annotation processor logic, select Rebuild Project, and trigger the annotation processor processing logic to implement breakpoint debugging.