Words: 3790 words
Estimated reading time: 25 minutes
Read the guide
APT(Annotation Processing Tool) is a Tool for Processing annotations.
APT scans and processes annotations in the source code during compilation. According to annotations, APT can be used to automatically generate Java code during development, which reduces redundant code and manual code input process, improves coding efficiency, and makes the source code look clearer, concise and readable.
At present, many third-party open source frameworks use APT technology to reduce the duplication of developers. Common examples are ButterKnife, EventBus, etc.
This paper focuses on the explanation of practical applications. Taking a common page jump scenario in the development of Android APP as an example, it explains how to complete the development of AN APT project step by step from the process of building a project, APT data and function introduction, data extraction and automatic code generation.
Directory:
1 |
The concept of APT A brief introduction to the concept of APT and its current application; |
2 |
Application Scenario Example Introduce a practical scenario and explain it step by step; |
3 |
Build APT Project How to build an APT project in Android Studio |
4 |
Data types and concepts in APT Brief introduction of APT common data types; |
5 |
APT process disassembly Break down APT process, how to get data needed for annotations; |
6 |
JavaPoet automates code generation How to generate.java code files based on the obtained annotation data; |
7 |
Summary of development process Schematic diagram of the whole process. |
❶ –
The concept of APT
APT (Annotation Processor Tool) is a built-in Tool of JavAC for scanning and processing annotations at compile time. To put it simply, during source code compilation, the annotation processor is used to retrieve the annotation content in the source file.
Since the annotation handler works at compile time, we can use the annotation handler to do what we need at compile time. A common usage is to get the relevant annotation data at compile time and then generate the code source file on the fly.
Usually the annotation processor is used to automatically generate some regular repeated code, which solves the problem of manual writing repeated code and greatly improves the coding efficiency.
Many of the better known open source frameworks use this technique, such as ButterKnife, which solves the problem of manually writing a large number of findViewById methods for developers. Others, such as THE JDT used in GreenDao, are exactly the same as APT, but the IDE and tools are different.
❷ –
Application Scenario Example
1. Requirement scenario
In Android development, a jump to an Activity is a necessary action. When data is passed in an Intent, the code looks like this:
1Intent intent = new Intent(context, TestActivity.class); 2intent.putExtra("id", id); 3intent.putExtra("name", name); 4intent.putExtra("is", is); 5if (! (context instanceof Activity)) {6 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 7}8startActivity(intent);
Copy the code
And get data in TestActivity:
1id = intent.getIntExtra("id", 0); 2name = intent.getStringExtra("name"); 3is = intent.getBooleanExtra("is", false);
Copy the code
The problem with the above code is that you need to write similar code for each procedure, which is repetitive and time-consuming.
Keys need to be consistent when data is passed and parsed. We could use constants instead, but we’re going to define a lot of constants.
We want the above code to be generated automatically, and developers need only call a few more readable methods to achieve the above process.
2. The analysis
For this requirement scenario, we need to implement the following automation functions:
(1) Automatically generate a class for TestActivity called TestActivityFastBundle;
(2) provide constructor mode chain call, can be needed to assign values to variables;
(3) Provide a build method that returns an Intent object;
(4) Can jump to the Activity, support startActivity or startActivityForResult;
(5) Support calling an interface to parse the data passed in the Intent and assign values to the Activity.
We expect the simplified call to look like this, which will jump to TestActivity:
1new TestActivityFastBundle()2 .id(1)3 .is(true)4 .name("user")5 .launch(this); // Or use launchForResult
Copy the code
In TestActivity, we expect to call:
1new TestActivityFastBundle().bind(this, getIntent());
Copy the code
The implementation automatically assigns variables in the Intent to variables in the current class.
❸ –
Build APT Project
1. Create an Android Library and create your own annotation classes.
For example:
1@Retention(CLASS)2@Target(FIELD)3public @interface AutoBundle {4 boolean require() default false; 5}
Copy the code
2. Create a Java Library, reference the Android Library created in Step 1, and add dependencies to the Java Library.
1 implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2'
Copy the code
Here’s what this library does:
Because annotation handlers work at compile time, they need to “register” with the compiler to let it know which annotator needs to be used to process data.
If the auto-service library is not used, the manual registration method is as follows:
1. Create the Resources folder in Library.
2. Create meta-INF and services folders in resources.
3. In the services to create a file, named javax.mail. The annotation. Processing. The Processor;
4. The javax.mail. The annotation. Processing. Processor file input created his annotation Processor class name (complete, including the package name).
3. Create your own processing class, inherit AbstractProcessor, and register it with Auto-service.
For example:
1@AutoService(Processor.class)2public class AutoBundleProcessor extends AbstractProcessor
Copy the code
Now that we subclass AbstractProcessor, we need to override a few of these methods to implement our own processing logic:
1@Override2public synchronized void init(ProcessingEnvironment processingEnvironment)3
Copy the code
The ProcessingEnvironment class contains the data objects that we need to parse, and we can use it to retrieve a list of other objects that we need to retrieve the data that we need.
1@Override2public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
Copy the code
The process method calls back during compilation to retrieve the classes, objects, and corresponding annotations we need, analyze and process the data, and ultimately generate the code we need.
1@Override2public Set<String> getSupportedAnnotationTypes()
Copy the code
GetSupportedAnnotationTypes method is needed to help us get comments. We put the class name we want into the Set and return it to the annotation handler, in other words, specify which annotations the annotation handler needs to work with.
4. Refer to it in your project
The Android Library that contains annotations is referenced in the main gradle project to reference the Java Library where the annotator is located. Because of kotlin’s introduction, it is recommended to use kapt instead of annotationProcessor.
For example:
1kapt project(':libProce')
Copy the code
At this point, the overall structure of the project has been built.
The following sections will introduce the functions of various classes and objects in APT and how to achieve the functions we need.
– ❹ –
Data types and concepts in APT
1.ProcessingEnvironment
When we override an AbstractProcessor init method in a subclass, the argument is a ProcessingEnvironment object. It internally provides useful objects such as Elements, Types, and Filer, all of which play an important role in APT. We can get these objects to do what we want.
2.Element
In APT, everything is called an element. An object, a class, a method, a parameter. In APT, they are all collectively called elements. Element itself is an interface, but there are subclasses, such as TypeElement and VariableElement, on which additional interface methods are added to describe special properties of specific things.
3.ElementKind
In APT, everything is called an element, so we need to know what an element is, and we can judge it by ElementKind.
ElementKind is an enumerated class. Including but not limited to PACKAGE, CLASS, INTERFACE, FIELD, PARAMETER, METHOD, etc. These are the basic concepts in our development.
4.Elements
Elements can be understood as a utility class that operates on an Element object and performs some processing or evaluation on it.
5.TypeElement
TypeElement is an Element subclass that indicates that the Element is a class or interface. When an Element meets the criteria, you can force it into a TypeElement object.
6.VariableElement
A VariableElement is a subclass of Element that indicates that the Element is a variable, constant, method, constructor, parameter, etc. When an Element satisfies a condition, it can be forcibly converted to a VariableElement object.
7.Filer
Filer is an interface for file operations that can create or write a Java file. Mainly for Java file objects, and the general file difference is that this is specialized in dealing with Java class files, with. Java or.class suffix files. In APT, if we need to generate a.java or.class file after automatic code generation, Filer is needed.
8.Name
The Name class is a subclass of CharSequence and represents the Name of the class and method. In most cases, you can think of it as equivalent to String.
9.Types
Types can be understood as a utility class, which is a type manipulation tool. In the APT phase, we need to know whether a variable is int or Boolean, and that will need to be handled by the Types related class. It can manipulate TypeMirror objects.
10.TypeMirror
TypeMirror indicates the data type. Basic types such as int and Boolean can also represent complex data types such as custom classes, arrays, and Parcelable.
11.Modifier
Modifiers. For example, when declaring a variable, private static final are all modifiers. Most of the ones that Android Studio colors blue are modifiers (except for class Int Interface).
Note: If a variable in a class has a default scope, the modifier is default.
12.RoundEnvironment
When we override AbstractProcessor’s process method in a subclass, the argument is a RoundEnvironment object. The Element that we annotated in our code is available through the RoundEnvironment object.
❺ –
APT process disassembly
Below, the scenarios mentioned above will be gradually disassembled for APT processing, and the required attributes will be finally obtained to prepare for the generation of automatic codes.
Set annotations on variables in TestActivity:
1@AutoBundle2public int id; 3@AutoBundle4public String name; 5@AutoBundle6public boolean is;
Copy the code
AutoBundle annotations are our own annotation classes.
After the initial design, we need to rewrite our logic in the process method:
1@Override2public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
Copy the code
The first step:
Gets all elements declared by the AutoBundle annotation. Here we know that there are really only three variables;
1for (Element element : roundEnvironment.getElementsAnnotatedWith(AutoBundle.class)) {23}
Copy the code
The second step:
For each Element object in the loop, get its data information;
1if (element.getkind () == elementkind.field) {2 VariableElement = (VariableElement) Element; 4 / / for variables in the class information TypeElement object 5 TypeElement TypeElement = (TypeElement) variableElement. GetEnclosingElement ();
Copy the code
The data contained in a variableElement includes modifiers, types, variable names, and so on;
TypeElement contains data such as class name, package name, and so on.
1 / / get the name of the class 2 string className = typeElement. GetSimpleName (), toString (); 5String packageName = elements.getpackageof (typeElement).getqualifiedname ().tostring (); 6 7 / / for annotations on the variable information 8 autoBundle autoBundle = variableElement. GetAnnotation (autoBundle. Class); 9boolean require = autoBundle.require(); 1011 / / get the variable name 12 name name = variableElement. GetSimpleName (); 15TypeMirror type = variableElement.astype ();
Copy the code
For a variable we defined above, for example:
1@AutoBundle(require = true)2public int id;
Copy the code
Then after obtaining the data:
1require = true2name = "id" 3type = int.class
Copy the code
The same goes for the other two variables.
Three cycles will get us all the information we need.
Includes the annotation value, variable name, and type of the three variables. We also get the class name and package name for TestActivity. Some encapsulation and caching of this data can be done. Now you can automate code generation.
I encapsulate the above variable values into ClassHoder and FieldHolder classes. ClassHolder holds the class name, package name, etc., and FieldHolder holds each variable type, variable name, annotation, etc. Now you’ll use this saved data to generate code from JavaPoet.
– ❻ –
JavaPoet code generation is automated
JavaPoet is Java code automatically generated framework, it is a lot of open source projects, address: https://github.com/square/javapoet.
JavaPoet simplifies the development of Java code generation and makes calls more user-friendly and readable through the builder mode. With automatic import function, do not need to specify manually.
In JavaPoet, most data types use common types in APT, which is very convenient and fast to generate code with APT automation.
1.TypeSpec.Builder
TypeSpec.Builder is the building class of classes, where the class is generalized and can be a class, interface, annotation, etc.
methods |
function |
classBuilder |
class |
annotationBuilder |
annotations |
interfaceBuilder |
interface |
anonymousClassBuilder |
An anonymous class |
Sample code:
1TypeSpec.Builder contentBuilder = TypeSpec.classBuilder("yourClassName")
Copy the code
2.MethodSpec.Builder
Methodspec.builder is the building class for methods.
methods |
function |
constructorBuilder |
A constructor |
methodBuilder |
methods |
Sample code:
1MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("yourMethodName")
Copy the code
3.FieldSpec.Builder
Fieldspec. Builder is the build class for variables.
methods |
function |
builder |
Create a variable |
Sample code:
1FieldSpec.Builder fieldBuilder = FieldSpec.builder(ClassName.get(field.getType()), "yourFieldName", Modifier.PRIVATE)
Copy the code
4.JavaFile.Builder
methods |
function |
builder |
Create a JavaFile object |
writeTo |
Write data as Java files, including Filer objects and paths |
Sample code:
1JavaFile javaFile = JavaFile.builder(classHolder.getPackageName(), contentBuilder.build())2javaFile.writeTo(mFiler);
Copy the code
5. All kinds of Builder methods
methods |
function |
describe |
addModifier |
Add modifiers |
Modifier Corresponds to the Modifier class in Java, and all variables in the class are available. Such as private, public, protected, static, final, etc. Example: addModifiers(Modifier.PUBLIC) |
addParameter |
Add parameters |
Parameters in a method. Example: addParameter(ClassName.get(“android.content”, “Intent”), “intent”) addParameter(int.class, “requestCode”) |
addStatement |
Add a description |
Add code statements directly. Example: addStatement(“return this”) |
addCode |
Add code text |
Add code statements directly. The difference from addStatement is that addStatement is handled only in plain text, while addCode is handled in code language. AddCode will automatically import the classes used and add a semicolon to the end of the statement. Example: addCode(“if (! (context instanceof $T)) {\n”, ClassName.get(“android.app”, “Activity”)); |
returns |
Add return value |
Add the return value for the method. Example: returns(void.class); returns(ClassName.get(“android.content”, “Intent”)); |
addMethod |
Add methods |
Adds a constructed method object to the class. Example: addMethod(methodBuilder.build()); |
addField |
Add a variable |
Add a variable to a class. Example: addField(fieldBuilder.build()); |
6. Code generation examples
Constructing code and generating results Example 1:
1for (FieldHolder field : fields) {2 FieldSpec f = FieldSpec.builder(ClassName.get(field.getType()), field.getName(), Modifier.PRIVATE)3 .build(); 4contentBuilder.addField(f);
Copy the code
1private int id; 2private String name; 3private boolean is;
Copy the code
Constructing code and generating results Example 2:
1MethodSpec.Builder buildMethodBuilder = MethodSpec.methodBuilder("build") 2 .addModifiers(Modifier.PUBLIC) 3 .returns(ClassName.get("android.content", "Intent")); 4 5buildMethodBuilder.addParameter(ClassName.get("android.content", "Context"), "context"); 6 7buildMethodBuilder.addStatement(String.format("Intent intent = new Intent(context, %s.class)", classHolder.getClassName())); 8 9for (FieldHolder field : fields) {10 buildMethodBuilder.addStatement(String.format("intent.putExtra(\"%s\", %s)", field.getName(), field.getName())); 11}1213buildMethodBuilder.addCode("if (! (context instanceof $T)) {\n", ClassName.get("android.app", "Activity")); 1415buildMethodBuilder.addStatement("intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)"); 1617buildMethodBuilder.addCode("}\n"); 1819buildMethodBuilder.addStatement("return intent"); 2021contentBuilder.addMethod(buildMethodBuilder.build());
Copy the code
1public Intent build(Context context) { 2 Intent intent = new Intent(context, TestActivity.class); 3 intent.putExtra("id", id); 4 intent.putExtra("name", name); 5 intent.putExtra("is", is); 6 if (! (context instanceof Activity)) { 7 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 8 } 9 return intent; 10}
Copy the code
Constructing code and generating results Example 3:
1String fieldTypeName = field.getType().toString(); 2 3if (int.class.getName().equals(fieldTypeName) 4 || Integer.class.getName().equals(fieldTypeName)) { 5 6 builder.addStatement(String.format("target.%s = intent.getIntExtra(\"%s\", 0)", field.getName(), field.getName())); 7 8} else if (String.class.getName().equals(fieldTypeName)) { 910 builder.addStatement(String.format("target.%s = intent.getStringExtra(\"%s\")", field.getName(), field.getName())); 1112} else if (boolean.class.getName().equals(fieldTypeName)13 || Boolean.class.getName().equals(fieldTypeName)) {1415 builder.addStatement(String.format("target.%s = intent.getBooleanExtra(\"%s\", false)", field.getName(), field.getName())); 1617}
Copy the code
1public void bind(TestActivity target, Intent intent) {2 target.id = intent.getIntExtra("id", 0); 3 target.name = intent.getStringExtra("name"); 4 target.is = intent.getBooleanExtra("is", false); 5}
Copy the code
7. Write the generated code into a file
1JavaFile javaFile = JavaFile.builder(classHolder.getPackageName(), contentBuilder.build()).build(); 23try {4 javaFile.writeTo(mFiler); 5} catch (IOException e) {6 e.printStackTrace(); 7}
Copy the code
Construct a JavaFile object, add the TypeSpecBuilder content, and write it to the Filer. Compiled this file was generated under the corresponding package, as shown in figure, automatically generated files in the build/generated under/source/kapt (compiled using kapt commands).
Generated code:
❼ –
Summary of development process
Fox friends technical team other wonderful articles
Overview of distributed tracking systems and comparison of mainstream open source systems
Don’t understand how GIF loading works? Just look at me!
Android permissions, do you really know that?
Swift Codable combat skills
AspectJ in Android
Join sohu technology author day group
Thousand yuan payment waiting for you!
Stamp here! ☛