Compile-time annotations (APT) are divided into three parts:

  1. Custom annotation handlers: for example, ButterKnife and Room generate new classes from annotations;
  2. Use JcTree to modify code at compile time: Lombok automatically adds getter/setter methods to classes, inserts lines into methods, and so on.
  3. Custom Gradle plug-ins modify code at compile time: some code stub frameworks, for example, and some of our applications use this approach.

This article, in the form of a Demo, shows you how to create a custom annotation handler from scratch and generate a new class. This class has a static method that returns all classes with custom annotations added. Read this article and you’ll be able to write your own ButterKnife

The source code for this article can be viewed here: github.com/Sino-Snack/…


1. Environment setup and Gradle configuration

We create a New Java Library in the project. The Module name is defined as an Annotation. Define a custom annotation class:

@Target(ElementType.TYPE)
public @interface DemoAnnotation {
}

Copy the code

This is the end of the first step. (If you are not sure about the use of meta annotations, you can search for other articles.)

Create a Java Library called AnnotationProcessor in the project, and add the following dependencies to build.gradle:

import org.gradle.internal.jvm.Jvm apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // The Annotation module we just defined implementation Project (":Annotation") // Google's AutoService makes our Annotation handler register automatically Implementation 'com. Google. Auto. Services: auto - service: 1.0 rc4' / / is used to generate new classes, functions, implementation "com. Squareup: javapoet: 1.9.0" / / a tool library implementation of Google "com. Google. Guava guava: 24.1 - the jre" implementation files (Jvm. Current () toolsJar)} SourceCompatibility = "1.8" targetCompatibility = "1.8"Copy the code

Build. Gradle build. Gradle android-apt build.

buildscript { repositories { ... } dependencies { ... The classpath "com. Neenbedankt. Gradle. Plugins: android - apt: 1.8"}... }Copy the code

2. Implement custom annotation handlers

All custom annotation handlers should inherit from AbstractProcessor classes. We also define a handler and implement several template methods:

@AutoService(Processor.class) public class DemoProcessor extends AbstractProcessor { /* ======================================================= */ /* Fields */ /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * / / * * * are used to create the kind of written to the file * / private Filer mFiler; /* ======================================================= */ /* Override/Implements Methods */ /* ======================================================= */ @Override public synchronized void init(ProcessingEnvironment  environment) { super.init(environment); mFiler = environment.getFiler(); } @Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {// This method is the core of the annotation processor. } @ Override public Set < String > getSupportedAnnotationTypes () {/ / this method returns the current processor can handle what annotations, Here we only return DemoAnnotation return Collections. The singleton (DemoAnnotation. Class. GetCanonicalName ()); } @ Override public SourceVersion getSupportedSourceVersion () {/ / this method returns the current processor support return code version SourceVersion.latestSupported(); }}Copy the code

Our requirement is to generate a new class with a static method that returns all classes with the @Annotation Annotation added. These operations need to be implemented in the process() method. Steps: (1) Get all annotated elements; (2) Generate a method whose code block returns the list obtained in (1). (3) Create a class and add the methods generated in (2) to the class; (4) Write the class generated in (3) to a file.

So we get an implementation of this method:

@Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment environment) {// Get all classes annotated by @demoannotation set <? extends Element> elements = environment.getElementsAnnotatedWith(DemoAnnotation.class); // Create a method that returns Set<Class> MethodSpec method = createMethodWithElements(elements); // Create a class TypeSpec clazz = createClassWithMethod(method); // Write this class to the file writeClassToFile(clazz); return false; }Copy the code

Let’s take a look at how these three key methods are implemented:

2.2 How do I Create a Method

/** * Creates a method that returns information about all classes in Elements. */ private MethodSpec createMethodWithElements(Set<? Extends Element> elements) {// "getAllClasses" is the name of the generated method methodSpec.Builder Builder = MethodSpec.methodBuilder("getAllClasses"); // Add the "public static" Modifier to this method builder.addmodifiers (Modifier. Public, Modifier. / / define the type of the return value is Set < Class > ParameterizedTypeName returnType = ParameterizedTypeName. Get (ClassName. Get (Set. The Class), ClassName.get(Class.class) ); builder.returns(returnType); Public static Set<Class> getAllClasses() {} public static Set<Class> getAllClasses() {} Set<Class> set = new HashSet<>(); builder.addStatement("$T<$T> set = new $T<>();" , Set.class, Class.class, HashSet.class); // The "$T" above is a placeholder for a type that can automatically import packages. $L: Literals, $S: String, $N: Names // Iterate over elements, add line for (Element Element:) ClassType ClassType type = (ClassType) element.astype (); // Add a line of code to the method we created: set.add(xxx.class); builder.addStatement("set.add($T.class)", type); } // After the for loop above, we add all the annotated classes to the set variable. // Finally, we just need to return the set as the return value: Builder.addStatement ("return set"); return builder.build(); }Copy the code

2.3 How do I Create a New Class

/** * create a class, Private TypeSpec createClassWithMethod(MethodSpec Method) {// Define a class called OurClass TypeSpec.Builder ourClass = TypeSpec.classBuilder("OurClass"); // Declare the Modifier public ourClass.addmodifiers (Modifier. Public); Ourclass.addjavadoc (" this class is automatically created! ~\n\ n@author ZhengHaiPeng"); // Add a new method to this class ourClass.addMethod(method); return ourClass.build(); }Copy the code

2.4 How do I write a created class to a file

Private void writeClassToFile(TypeSpec clazz) {// Declare a file in "me.moolv.apt" JavaFile file = JavaFile.builder("me.moolv.apt", clazz).build(); // Write the file try {file.writeto (mFiler); } catch (IOException e) { e.printStackTrace(); }}Copy the code

3. Use custom annotation handlers

Add dependencies to build.gradle in the Module you want to use, such as app:

apply plugin: 'com.android.application'

android {
    ...
}

dependencies {
    ...
    annotationProcessor project(":AnnotationProcessor")
    implementation project(path: ':Annotation')
}

Copy the code

Android Studio Build > Make Project, app Module Build /source/apt path to find the generated class file:

Public class OurClass {public static Set< class > getAllClasses() {public static Set< class > getAllClasses() { Set<Class> set = new HashSet<>(); set.add(MainActivity.class); return set; }}Copy the code

So we have a custom annotation processor, and generate code, have a question to leave a message ~


4. How do I pass parameters to the annotation handler?

The Processor in APT may use some parameters that can be configured in Gradle.

Set the parameters

android { ... defaultConfig { ... JavaCompileOptions {annotationProcessorOptions {/ / the following definition to the parameters passed argument "key1", "value1" argument "key2", "value2" } } }Copy the code

In the Processor init method, you can get parameters:

@Override public synchronized void init(ProcessingEnvironment env) { super.init(env); . String value1 = env.getOptions().get("key1"); . }Copy the code