@What is the principle behind lombok annotations? Let's get closer to the custom Java annotation processor

Keywords: Java

This article introduces how to customize the Java annotation processor and related knowledge. After reading this article, you can easily understand and understand the application of the annotation processor of major open source frameworks.

"Garden tour is not worth it"
Should pity clogs tooth print moss, small buckle firewood door can't open for a long time.
Spring is full and the garden can't be closed. A red apricot comes out of the wall.
-Song, ye Shaoweng

This article starts: http://yuweiguocn.github.io/

For custom Java annotations, see Custom annotation.

This article has authorized WeChat official account: hongyangAndroid.

Basic implementation

There are two steps to implement a custom annotation Processor. The first is to implement the Processor interface to process annotations, and the second is to register the annotation Processor.

Implement the Processor interface

You can customize the annotation Processor by implementing the Processor interface. Here, we use a simpler method to implement the custom annotation Processor by inheriting the AbstractProcessor class. Implement the abstract method process to handle the functions we want.

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
}

In addition, we also need to specify the supported annotation types and supported Java versions. By overriding the getSupportedAnnotationTypes method and getSupportedSourceVersion method:

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(CustomAnnotation.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

For specifying supported annotation types, we can also specify them by annotation:

@SupportedAnnotationTypes({"io.github.yuweiguocn.annotation.CustomAnnotation"})
public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

Because the Android platform may have compatibility problems, it is recommended to override the getSupportedAnnotationTypes method to specify the supported annotation types.

Register annotation processor

Finally, we need to register our custom annotation processor. Create a new res folder, a new META-INF folder under the directory, a new services folder under the directory, a new javax.annotation.processing.Processor file under the directory, and then write the full class name of our custom annotation processor to this file:

io.github.yuweiguocn.processor.CustomProcessor

The above registration method is too troublesome. Google helped us write an annotation processor to generate this file.
github address: https://github.com/google/auto
Add dependency:

compile 'com.google.auto.service:auto-service:1.0-rc2'

Add comments:

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    ...
}

Get it done and realize the power of annotation processor. Later, we only need to focus on the processing logic in the annotation processor.

Let's take a look at the final project structure:

Basic concepts

There is also an init method in the abstract class, which is provided in the Processor interface. When we compile the program, the annotation Processor tool will call this method and provide the object implementing the ProcessingEnvironment interface as a parameter.

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
}

We can use ProcessingEnvironment to obtain some utility classes and option parameters:

methodexplain
Elements getElementUtils()Returns the object that implements the Elements interface and the tool class used to operate the Elements.
Filer getFiler()Returns the object that implements the Filer interface, which is used to create files, classes, and auxiliary files.
Messager getMessager()Returns the object implementing the Messager interface, which is used to report error messages, warnings and reminders.
Map<String,String> getOptions()Returns the specified parameter options.
Types getTypeUtils()Returns the object that implements the Types interface, which is used to manipulate the tool class of the type.

element

The Element element is an interface that represents a program Element, such as a package, class, or method. All of the following Element type interfaces inherit from the Element interface:

typeexplain
ExecutableElementRepresents a method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements.
PackageElementRepresents a package element. Provides access to information about packages and their members.
TypeElementRepresents a class or interface program element. Provides access to information about types and their members. Note that the enumeration type is a kind, and the annotation type is an interface.
TypeParameterElementA formal type parameter that represents a general class, interface, method, or constructor element.
VariableElementRepresents a field, enum constant, method or constructor parameter, local variable, or exception parameter.

If we want to judge the type of an element, we should use the Element.getKind() method in conjunction with the ElementKind enumeration class. Try to avoid using instanceof for judgment, because for example, TypeElement represents both a class and an interface. The judgment result may not be what you want. For example, we judge whether an element is a class:

if (element instanceof TypeElement) { //Error, or it may be an interface
}

if (element.getKind() == ElementKind.CLASS) { //correct
    //doSomething
}

The following table shows some constants in ElementKind enumeration class. Please refer to the official document for details.

typeexplain
PACKAGEA bag.
ENUMAn enumeration type.
CLASSThere are no classes described in more specific categories, such as ENUM.
ANNOTATION_TYPEAn annotation type.
INTERFACEThere is no interface described in a more special kind, such as ANNOTATION_TYPE.
ENUM_CONSTANTAn enumeration constant.
FIELDThere are no fields described in a more special kind, such as ENUM_CONSTANT.
PARAMETERMethod or constructor.
LOCAL_VARIABLELocal variables.
METHODOne way.
CONSTRUCTORA construction method.
TYPE_PARAMETERA type parameter.

type

TypeMirror is an interface that represents types in the Java programming language. These types include base types, declaration types (class and interface types), array types, type variables, and null types. You can also represent wildcard type parameters, the signature and return type of executable, and pseudo types corresponding to package and keyword void. All of the following type interfaces inherit from the TypeMirror interface:

typeexplain
ArrayTypeRepresents an array type. Multidimensional array types are represented as component types and array types of array types.
DeclaredTypeRepresents a declaration type, which is a class type or interface type. This includes parameterized types (such as Java. Util. Set < string >) and primitive types. TypeElement represents a class or interface element, while DeclaredType represents a class or interface type, which will become a use (or call) of the former.
ErrorTypeRepresents a class or interface type that cannot be modeled properly.
ExecutableTypeRepresents the type of executable. Executable is a method, constructor, or initializer.
NoTypeA pseudo type used where the actual type does not fit.
NullTypeRepresents a null type.
PrimitiveTypeRepresents a basic type. These types include boolean, byte, short, int, long, char, float, and double.
ReferenceTypeRepresents a reference type. These types include class and interface types, array types, type variables, and null types.
TypeVariableRepresents a type variable.
WildcardTypeRepresents a wildcard type parameter.

Similarly, if we want to judge the type of a TypeMirror, we should use the TypeMirror.getKind() method in conjunction with the TypeKind enumeration class. Try to avoid using instanceof for judgment, because for example, DeclaredType represents both class type and interface type. The judgment result may not be what you want.

Some constants in TypeKind enumeration class. Please check the official documentation for details.

typeexplain
BOOLEANThe basic type is boolean.
INTBasic type int.
LONGThe basic type is long.
FLOATThe basic type is float.
DOUBLEThe basic type is double.
VOIDThe pseudo type corresponding to the keyword void.
NULLnull type.
ARRAYArray type.
PACKAGEThe pseudo type corresponding to the package element.
EXECUTABLEMethod, constructor, or initializer.

create a file

The Filer interface supports the creation of new files through the annotation processor. You can create three file types: source files, class files, and auxiliary resource files.

1. Create source file

JavaFileObject createSourceFile(CharSequence name,
                                Element... originatingElements)
                                throws IOException

Create a new source file and return an object to allow it to be written. The name and path of the file (relative to the root directory output location of the source file) are based on the types declared in the file. If you declare more than one type, you should use the name of the main top-level type (for example, the one declared public). You can also create source files to hold information about a package, including package annotations. To create a source file for the specified package, you can use name as the package name followed by ". Package info"; To create a source file for an unspecified package, use "package info".

2. Create class file

JavaFileObject createClassFile(CharSequence name,
                               Element... originatingElements)
                               throws IOException

Create a new class file and return an object to allow it to be written. The name and path of the file (relative to the root directory output location of the class file) are based on the type name to be written. You can also create class files to hold information about a package, including package annotations. To create a class file for the specified package, you can use name as the package name followed by ". Package info"; Creating class files for unspecified packages is not supported.

3. Create auxiliary resource file

FileObject createResource(JavaFileManager.Location location,
                          CharSequence pkg,
                          CharSequence relativeName,
                          Element... originatingElements)
                          throws IOException

Create a new auxiliary resource file for the write operation and return a file object for it. The file can be found with a newly created source file, a newly created binary file, or other supported location. Location CLASS_OUTPUT and SOURCE_OUTPUT must be supported. Resources can be specified relative to a package (which is a source file and class file) and extracted from it by relative pathname. From a less strict point of view, the full pathname of the new file will be a concatenation of location, pkg, and relativeName.

For generating Java files, you can also use Square's open source class library JavaPoet , interested students can understand.

Print error message

The Messager interface provides a way for the annotation processor to report error messages, warnings, and other notifications.

Note: we should catch the possible exceptions during processing and notify the user through the method provided by the Messager interface. In addition, using the method with Element parameter to connect to the Element with error, the user can directly click the error message and jump to the corresponding line of the error source file. If you throw an exception in process(), the JVM running the annotation processor will crash (just like other Java applications), so that users will get a very difficult error message from javac.

methodexplain
void printMessage(Diagnostic.Kind kind, CharSequence msg)Print messages of the specified kind.
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e)Prints a message of the specified kind at the location of the element.
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a)Prints a message of the specified kind at the annotation mirror position of the annotated element.
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v)Prints a message of the specified kind at the location of the annotation image internal annotation value of the annotated element.

Configure option parameters

We can get the option parameters through the getOptions() method and configure the option parameter values in the gradle file. For example, we configured a parameter value called yuweiguoCustomAnnotation.

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ yuweiguoCustomAnnotation : 'io.github.yuweiguocn.customannotation.MyCustomAnnotation' ]
            }
        }
    }
}

Override the getSupportedOptions method in the annotation processor to specify the name of the supported option parameter. Get the option parameter value through the getOptions method.

public static final String CUSTOM_ANNOTATION = "yuweiguoCustomAnnotation";

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           ...
           return false;
       }
       ...
   } catch (Exception e) {
       e.printStackTrace();
       ...
   }
   return true;
}

@Override
public Set<String> getSupportedOptions() {
   Set<String> options = new LinkedHashSet<String>();
   options.add(CUSTOM_ANNOTATION);
   return options;
}

Processing process

The definition of annotation processing given in the official Java document: annotation processing is an orderly circular process. In each loop, a processor may be required to process the annotations in the source and class files generated in the previous loop. The input for the first cycle is the initial input for running the tool. These initial inputs can be regarded as the output of the virtual 0th cycle. This means that the process method we implement may be called many times, because the file we generate may also contain corresponding annotations. For example, our source file is SourceActivity.class and the generated file is Generated.class. In this way, there will be three cycles. The first input is SourceActivity.class and the output is Generated.class; The second input is Generated.class, and the output does not generate a new file; The third input is null and the output is null.

Each cycle will call the process method, which provides two parameters. The first is the collection of annotation types we request to process (that is, the annotation type we specify by overriding the getSupportedAnnotationTypes method), and the second is the environment of information about the current and last cycle. The return value indicates whether these annotations are declared by this Processor. If true is returned, these annotations have been declared and subsequent processors are not required to process them; If false is returned, these annotations are undeclared and may require subsequent processors to process them.

public abstract boolean process(Set<? extends TypeElement> annotations,
                                RoundEnvironment roundEnv)

Get annotation element

We can get annotation elements through the RoundEnvironment interface. The process method provides an object that implements the RoundEnvironment interface.

methodexplain
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)Returns the collection of elements annotated by the specified annotation type.
Set<? extends Element> getElementsAnnotatedWith(TypeElement a)Returns the collection of elements annotated by the specified annotation type.
processingOver()Return true if the loop processing is completed, otherwise return false.

Example

After understanding the basic concepts, let's take a look at an example. This example is only for demonstration and has no practical significance. The main function is to customize an annotation, which can only be used on public methods. We get the class name and method name through the annotation processor and store them in the List collection, and then generate a file specified through the parameter options. Through this file, we can obtain the List collection.

Custom annotation:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

Key codes in annotation processor:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           messager.printMessage(Diagnostic.Kind.ERROR, "No option " + CUSTOM_ANNOTATION +
                   " passed to annotation processor");
           return false;
       }

       round++;
       messager.printMessage(Diagnostic.Kind.NOTE, "round " + round + " process over " + roundEnv.processingOver());
       Iterator<? extends TypeElement> iterator = annotations.iterator();
       while (iterator.hasNext()) {
           messager.printMessage(Diagnostic.Kind.NOTE, "name is " + iterator.next().getSimpleName().toString());
       }

       if (roundEnv.processingOver()) {
           if (!annotations.isEmpty()) {
               messager.printMessage(Diagnostic.Kind.ERROR,
                       "Unexpected processing state: annotations still available after processing over");
               return false;
           }
       }

       if (annotations.isEmpty()) {
           return false;
       }

       for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
           if (element.getKind() != ElementKind.METHOD) {
               messager.printMessage(
                       Diagnostic.Kind.ERROR,
                       String.format("Only methods can be annotated with @%s", CustomAnnotation.class.getSimpleName()),
                       element);
               return true; // Exit processing
           }

           if (!element.getModifiers().contains(Modifier.PUBLIC)) {
               messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must be public", element);
               return true;
           }

           ExecutableElement execElement = (ExecutableElement) element;
           TypeElement classElement = (TypeElement) execElement.getEnclosingElement();
           result.add(classElement.getSimpleName().toString() + "#" + execElement.getSimpleName().toString());
       }
       if (!result.isEmpty()) {
           generateFile(resultPath);
       } else {
           messager.printMessage(Diagnostic.Kind.WARNING, "No @CustomAnnotation annotations found");
       }
       result.clear();
   } catch (Exception e) {
       e.printStackTrace();
       messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in CustomProcessor: " + e);
   }
   return true;
}

private void generateFile(String path) {
   BufferedWriter writer = null;
   try {
       JavaFileObject sourceFile = filer.createSourceFile(path);
       int period = path.lastIndexOf('.');
       String myPackage = period > 0 ? path.substring(0, period) : null;
       String clazz = path.substring(period + 1);
       writer = new BufferedWriter(sourceFile.openWriter());
       if (myPackage != null) {
           writer.write("package " + myPackage + ";\n\n");
       }
       writer.write("import java.util.ArrayList;\n");
       writer.write("import java.util.List;\n\n");
       writer.write("/** This class is generated by CustomProcessor, do not edit. */\n");
       writer.write("public class " + clazz + " {\n");
       writer.write("    private static final List<String> ANNOTATIONS;\n\n");
       writer.write("    static {\n");
       writer.write("        ANNOTATIONS = new ArrayList<>();\n\n");
       writeMethodLines(writer);
       writer.write("    }\n\n");
       writer.write("    public static List<String> getAnnotations() {\n");
       writer.write("        return ANNOTATIONS;\n");
       writer.write("    }\n\n");
       writer.write("}\n");
   } catch (IOException e) {
       throw new RuntimeException("Could not write source for " + path, e);
   } finally {
       if (writer != null) {
           try {
               writer.close();
           } catch (IOException e) {
               //Silent
           }
       }
   }
}

private void writeMethodLines(BufferedWriter writer) throws IOException {
   for (int i = 0; i < result.size(); i++) {
       writer.write("        ANNOTATIONS.add(\"" + result.get(i) + "\");\n");
   }
}

Compile output:

Note: round 1 process over false
Note: name is CustomAnnotation
Note: round 2 process over false
Note: round 3 process over true

Get full code: https://github.com/yuweiguocn/CustomAnnotation

For uploading custom annotation processor to jcenter, please see Upload class library to jcenter.

I'm glad you can read here. At this time, go to see the source code of annotation processor in EventBus 3.0. I believe you can easily understand its principle.

Note: if you clone the project code, you may find that the annotation and annotation processor are separate modules. One thing is certain that our annotation processor only needs to be used during compilation and does not need to be packaged in APK. Therefore, for the sake of users, we need to separate the annotation processor into separate modules.

reference resources

Author: Yu Weiguo
Link: https://www.jianshu.com/p/50d...
Source: Jianshu
The copyright of Jianshu belongs to the author. Please contact the author for authorization and indicate the source for any form of reprint.

Posted by Iconoclast on Tue, 23 Nov 2021 00:17:27 -0800