This is the 8th day of my participation in the More text Challenge. For details, see more text Challenge
preface
Nice to meet you
In the last article in this series, we talked about annotations. If you haven’t seen the last article, you are advised to read the APT series for Android (part 2) : Annotations for APT Building Foundation. So far, we have finished the basic part of Apt, and then we will formally enter the study of Apt technology
Github Demo address, you can see the Demo to follow my ideas together analysis
Introduction to APT
1) What is APT?
APT is the full name Annotation Processing Tool. APT is a tool for handling comments. It detects source code files for comments and uses them for additional processing.
2) What is APT for?
APT can be annotated at compile time, giving us automatically generated code to simplify usage. APT technology is used by many popular frameworks such as ButterKnife, Retrofit, Arouter, EventBus, etc
2. APT Project
1) APT project creation
In general, APT has a rough implementation process:
Create a Java Module for writing annotations
Create a Java Module that reads the annotation information and generates the corresponding class file according to the specified rules
3. Create an Android Module, obtain the generated class through reflection, encapsulate it reasonably, and provide it to the upper layer for invocation
The diagram below:
This is my APT project, about the Module name can be arbitrary, according to the rules I said above to proceed
2) Module dependencies
Once the project is created, we need to clarify a dependency between each Module:
1. Because apt-processor needs to read the annotations of apT-annotation, apt-processor needs to rely on apt-annotation
//apt-processor build.gradle file
dependencies {
implementation project(path: ':apt-annotation')
}
Copy the code
2. As the calling layer, app needs to rely on the above three modules
// App build.gradle file
dependencies {
/ /...
implementation project(path: ':apt-api')
implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')
}
Copy the code
APT project configuration is good, we can each Module for a specific code to write
3. Compiling apt-annotation
This Module is relatively easy to handle, just write the corresponding custom annotations, I wrote the following:
@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface AptAnnotation {
String desc(a) default "";
}
Copy the code
4. Apt-processor automatically generates code
This Module is relatively complex, and we divide it into the following three steps:
1. Annotate processor declarations
2. Annotate processor registration
3. The annotation processor generates class files
1) Annotate processor declarations
1, create a new class, the name of the class according to their preferences, inheritancejavax.annotation.processing
The AbstractProcessor class under this package and implements its abstract methods
public class AptAnnotationProcessor extends AbstractProcessor {
/** * Write the logic for generating Java classes **@paramSet supports the collection of annotations * to be processed@paramRoundEnvironment uses this object to find node information * under the specified annotation@returnTrue: indicates that the annotations are processed and no further processing is required by the annotation handler. False: indicates that the annotation has not been processed and may require a subsequent annotation handler to process */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false; }}Copy the code
The first argument is called TypeElement. The first argument is called TypeElement. The first argument is called TypeElement.
Element is introduced
In fact, Java source files are a structured language, where each part of the source code corresponds to a specific type of Element, such as package, class, field, method, and so on:
package com.dream; // PackageElement: PackageElement
public class Main<T> { // TypeElement: class element; Where
belongs to the TypeParameterElement generic element
private int x; // VariableElement: variable, enumeration, method parameter element
public Main(a) { // ExecuteableElement: constructor, method element}}Copy the code
Element in Java is an interface with the following source code:
public interface Element extends javax.lang.model.AnnotatedConstruct {
// Get the element type, the actual object type
TypeMirror asType(a);
// Get the Element type and determine which Element it is
ElementKind getKind(a);
// Get the modifier, such as public static final
Set<Modifier> getModifiers(a);
// Get the class name
Name getSimpleName(a);
// Return the parent containing the node, as opposed to the getEnclosedElements() method
Element getEnclosingElement(a);
// Return the child nodes directly contained under the node, such as the class node contained under the package node
List<? extends Element> getEnclosedElements();
@Override
boolean equals(Object obj);
@Override
int hashCode(a);
@Override
List<? extends AnnotationMirror> getAnnotationMirrors();
// Get the annotation
@Override
<A extends Annotation> A getAnnotation(Class<A> annotationType);
<R, P> R accept(ElementVisitor<R, P> v, P p);
}
Copy the code
We can get some of this information from the Element (annotated ones are the usual ones)
There are five extension classes derived from Element:
1. PackageElement represents a PackageElement
TypeElement represents a class or interface element
TypeParameterElement represents a generic element
4. VariableElement represents a field, enum constant, method or constructor parameter, local variable, or exception parameter
5. ExecuteableElement represents a method, constructor, or initializer (static or instance) for a class or interface
As you can see, an Element sometimes represents more than one Element. For example, TypeElement represents a class or an interface, which we can distinguish by element.getkind () :
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
if (element.getKind() == ElementKind.CLASS) {
// If the element is a class
} else if (element.getKind() == ElementKind.INTERFACE) {
// If the element is an interface}}Copy the code
ElementKind is an enumeration class that can take many values, such as:
PACKAGE / / package
ENUM // Represents enumeration
CLASS / / class
ANNOTATION_TYPE // Indicates annotations
INTERFACE // Indicates the interface
ENUM_CONSTANT // Represents an enumeration constant
FIELD // Indicates the field
PARAMETER // Indicates the parameter
LOCAL_VARIABLE // Represents a local variable
EXCEPTION_PARAMETER // Indicates the exception parameter
METHOD // Indicate the method
CONSTRUCTOR // Represents the constructor
OTHER // Other
Copy the code
With that said, let’s move on to Element
2. Override method interpretation
In addition to the abstract method we have to implement, there are four other commonly used methods we can override, as follows:
public class AptAnnotationProcessor extends AbstractProcessor {
/ /...
/** * Node utility class (class, function, property are nodes) */
private Elements mElementUtils;
/** * information tool class */
private Types mTypeUtils;
/** * File generator */
private Filer mFiler;
/** * Log message printer */
private Messager mMessager;
/** do some initialization work **@paramThe processingEnvironment parameter provides several utility classes for writing */ to use when generating Java classes
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementUtils = processingEnv.getElementUtils();
mTypeUtils = processingEnv.getTypeUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
/** * accept incoming parameters, most commonly in the form of javaCompileOptions configuration in the build.gradle script file **@returnProperty */
@Override
public Set<String> getSupportedOptions(a) {
return super.getSupportedOptions();
}
/** * Specifies the set of annotations supported by the current annotation handler. If so, the process method ** is called@returnSupported collection of annotations */
@Override
public Set<String> getSupportedAnnotationTypes(a) {
return super.getSupportedAnnotationTypes();
}
/** * compile the JDK version of the current annotation processor@return* / JDK version
@Override
public SourceVersion getSupportedSourceVersion(a) {
return super.getSupportedSourceVersion(); }}Copy the code
Note: getSupportedAnnotationTypes (), getSupportedSourceVersion getSupportedOptions and () () the three methods, we can also use annotations in a way that provides:
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
/ /...
}
Copy the code
2) Annotate processor registration
The annotation handler declaration is ready. The next step is to register it. There are two ways to register it:
1. Manual registration
2. Automatic registration
Manual registration is cumbersome and fixed and error prone, not recommended to use, here will not talk about. Let’s focus on automatic enrollment
Automatic registration
1. First, import the following dependencies into the build.gradle file under the apt-processor Module:
implementation 'com. Google. Auto. Services: auto - service: 1.0 - rc6'
annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 - rc6'
Copy the code
Note: these two sentences must be added, otherwise the registration will not succeed, I stepped on the pit before
2. Add @AutoService(processor.class) to the annotation Processor to complete registration
@AutoService(Processor.class)
public class AptAnnotationProcessor extends AbstractProcessor {
/ /...
}
Copy the code
3) annotate the handler to generate class files
After registration, we can formally write the code to generate the Java class files, which can also be generated in two ways:
1, the normal way to write files
2, through the Javapoet framework to write
1 the way is more rigid, need to write every letter, not recommended to use, here will not speak. We’ll focus on generating Java class files through the Javapoet framework
Javapoet way
This way is more in line with a style of object-oriented coding, for those who are not familiar with Javapoet, you can go to Github to learn a wave of portal, here we introduce some of its common classes:
TypeSpec: Class that generates classes, interfaces, and enumerations
MethodSpec: The class used to generate method objects
ParameterSpec: specifies the class used to generate parameter objects
AnnotationSpec: Class used to generate annotation objects
FieldSpec: Used to configure the class that generates member variables
ClassName: An object generated by the package name and Class name, which in JavaPoet is equivalent to specifying a Class for it
ParameterizedTypeName: generates a Class containing generic types using MainClass and IncludeClass
JavaFile: Class that controls the output of the generated Java file
1. Import javapoet framework dependencies
implementation 'com. Squareup: javapoet: 1.13.0'
Copy the code
2. Generate Java class files according to the specified code template
For example, I made the following configuration in app build.gradle:
android {
/ /...
defaultConfig {
/ /...
javaCompileOptions {
annotationProcessorOptions {
arguments = [MODULE_NAME: project.getName()]
}
}
}
}
Copy the code
The following comment is made under MainActivity:
The code I want to generate is as follows:
Now let’s put this into practice:
@AutoService(Processor.class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
// File generator
Filer filer;
/ / module name
private String mModuleName;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// Initialize the file generator
filer = processingEnvironment.getFiler();
// Obtain the corresponding value in build.gradle from key
mModuleName = processingEnv.getOptions().get("MODULE_NAME");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set == null || set.isEmpty()) {
return false;
}
// Get the node information under the current annotation
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
// Build the test function
MethodSpec.Builder builder = MethodSpec.methodBuilder("test")
.addModifiers(Modifier.PUBLIC) // Specify method modifiers
.returns(void.class) // Specify the return type
.addParameter(String.class, "param"); // Add parameters
builder.addStatement("$T.out.println($S)", System.class, "Module:" + mModuleName);
if(rootElements ! =null && !rootElements.isEmpty()) {
for (Element element : rootElements) {
// Name of the current node
String elementName = element.getSimpleName().toString();
// Attributes annotated under the current node
String desc = element.getAnnotation(AptAnnotation.class).desc();
// Build the body of the method
builder.addStatement("$T.out.println($S)", System.class,
"Node:" + elementName + "" + "Description:" + desc);
}
}
MethodSpec main =builder.build();
// Build the HelloWorld class
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC) // Specify the class modifier
.addMethod(main) // Add the method
.build();
// Specify the package path to build the body of the file
JavaFile javaFile = JavaFile.builder("com.dream.aptdemo", helloWorld).build();
try {
// Create the file
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
return true; }}Copy the code
After these steps, we can run the App and generate the code in the screenshot above. Now we need the final step, which is to use the generated code
Note: Gradle version 6.7.1 generates class files in different locations. My Gradle version 6.7.1 generates class files in the following locations:
Some earlier versions of Gradle generate class files in the /build/generated/source directory
5. Apt-api call generates code to complete business functions
The operation of this Module is relatively simple, that is, through reflection to obtain the generated class, the corresponding encapsulation can be used, I write as follows:
public class MyAptApi {
@SuppressWarnings("all")
public static void init(a) {
try {
Class c = Class.forName("com.dream.aptdemo.HelloWorld");
Constructor declaredConstructor = c.getDeclaredConstructor();
Object o = declaredConstructor.newInstance();
Method test = c.getDeclaredMethod("test", String.class);
test.invoke(o, "");
} catch(Exception e) { e.printStackTrace(); }}}Copy the code
Next, we call it in the onCreate method of MainActivity:
Notation (@aptanNotation = "I am the notation on MainActivity ")
public class MainActivity extends AppCompatActivity {
@aptannotation (desc = "I am the notation above onCreate ")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyAptApi.init(); }}// Print the resultModule: APP node: MainActivity Description: I am the annotation node above MainActivity: onCreate Description: I am the annotation above onCreateCopy the code
Six, summarized
Some of the highlights of this article:
1, APT project needs to create different types of modules and Module dependencies
Java source files are really a structured language, with each part of the source code corresponding to a specific type of Element
3. Use auto-service to automatically register the annotation processor
4. Use Javapoet framework to write Java class files needed to be generated
5. Through reflection and appropriate encapsulation, provide the generated class’s function to the upper level call
Well, that’s the end of this article, hope to help you 🤝
Thanks for reading this article
The next trailer
In the next article, I’ll show you how I applied APT technology to create a replacement for a View created by reflection, 😄
References and Recommendations
APT technology exploration of Android annotation processor
Here is the full text, the original is not easy, welcome to like, collect, comment and forward, your recognition is the motivation of my creation
Follow my public account, search for sweereferees on wechat and updates will be received as soon as possible