Development of plug-ins for automatically generating getter and setter methods based on annotation processor

Keywords: Java Lombok jvm Google

The copyright of this article belongs to the official account of WeChat public code ID:onblog. If it is reproduced, please keep the original statement of the paragraph, and the offender will investigate it. If there are any shortcomings, please welcome the official account of WeChat public. ]

Yesterday, I found out by accident lombok The author of the video just wrote a few fields in the entity class, which can be automatically compiled into a class file containing setter, getter, toString() and other methods. Looking at it is quite novel, so I studied the principle and sorted it out.

1. Where to start

The author's process in the video is as follows:

(1) Write Java file and @ Data annotation on class

@Data
public class Demo {
    private String name;
    private double abc;
}

(2)javac compilation, lombok.jar It's Lombok's jar package.

javac -cp lombok.jar Demo.java

(3)javap view Demo.class Class file

javap Demo

Demo.class:

public class Demo {
  public Demo();
  public java.lang.String getName();
  public void setName(java.lang.String);
  public double getAbc();
  public void setAbc(double);
}

You can see Demo.class There are many undefined setter and getter methods in the video, and the video author mainly uses annotation + compilation, so let's start from this aspect.

2. Necessary knowledge

2.1 notes

I believe that most people have used annotations. Many people will customize annotations and use reflection to make small things. But this article is not about using annotations and reflection to customize behavior at run time, but at compile time.

Four meta annotations are indispensable for custom annotation.

@Retention: annotation retention period

Retention type explain
SOURCE Only reserved in the source code, the compiled class does not exist
CLASS Keep in the class file, but the JVM will not load
RUNTIME It always exists. The JVM will be loaded and can be obtained by reflection

@Target: used to mark which types can be applied

Element type Applicable occasions
ANOTATION_TYPE Annotation type declaration
PACKAGE package
TYPE Class, enumeration, interface, annotation
METHOD method
CONSTRUCTOR Construction method
FIELD Member fields, enumerating constants
PARAMETER Method or constructor parameters
LOCAL_VARIABLE local variable
TYPE_PARAMETER Type parameter
TYPE_USE Type usage

@Documented: the function is to be able to include elements in annotations in Javadoc

@Inherited: inherited. Suppose annotation A uses this annotation, class B uses annotation A, class C inherits class B, and class C also uses annotation A. (used here to distinguish easy to understand, actually annotated)

2.1 annotation processor

Annotation processor is a special tool for processing annotations in javac package. All annotation processors must inherit the abstract class AbstractProcessor and override several of its methods.

The annotation processor is running in its own JVM. javac starts a full java virtual machine to run the annotation processor, which means you can use anything you use in other java applications. The abstract method process must be rewritten. In this method, the annotation processor can traverse all the source files, and then obtain all the elements marked by the annotation we need to process through the RoundEnvironment class. The elements here can represent package, class, interface, method, attribute, etc. In the process of reprocessing, specific tool classes can be used to automatically generate specific. java files or. Class files to help us deal with custom annotations.

A common annotation processor file is as follows:

package com.example;

import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add("com.example.MyAnnotation");
        return annotataions;
    }

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

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

}
  • init(ProcessingEnvironment processingEnv): all annotation processor classes must have a parameterless constructor. However, there is a special method, init(), which is called by the annotation processing tool with ProcessingEnvironment as the parameter. ProcessingEnvironment provides some utility classes, such as Elements, Types and filers.
  • Process (set <? Extensions typeelement > announcements, RoundEnvironment Env): This is similar to the main() method of each processor. You can code in this method to scan, process annotations, and generate java files. Using the RoundEnvironment parameter, you can query the elements annotated by a specific annotation.
  • getSupportedAnnotationTypes(): in this method you must specify which annotations should be registered by the annotation handler. Note that its return value is a String collection that contains the full name of the annotation type your annotation processor wants to handle. In other words, here you define which annotations your annotation processor will process.
  • getSupportedSourceVersion(): used to specify the java version you are using. Usually you should go back SourceVersion.latestSupported() . However, if you have enough reasons to stick with java 6, you can also return SourceVersion.RELEASE_6.

For the two methods getSupportedAnnotationTypes() and getSupportedSourceVersion(), you can also use the corresponding annotation instead. The code is as follows:

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
....

However, in order to be compatible with Java 6, it is better to overload these two methods.

3. Start coding

We have learned knowledge and now we are going to fight.

3.1 user defined notes

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Data {

}

3.2 custom annotation processor

public class DataAnnotationProcessor extends AbstractProcessor {
    private Messager messager; //For printing logs
    private Elements elementUtils; //For processing elements
    private Types typeUtils;
    private Filer filer;  //Used to create java files or class files

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        typeUtils = processingEnvironment.getTypeUtils();
    }
    
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> set = new HashSet<>();
        set.add(Data.class.getCanonicalName());
        return Collections.unmodifiableSet(set);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE,"-----Start automatic source generation");
        try {
            // identifier 
            boolean isClass = false;
            // Fully qualified name of class
            String classAllName = null;
            // Return annotated node
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Data.class);
            Element element = null;
            for (Element e : elements) {
                // If the comment is on a class
                if (e.getKind() == ElementKind.CLASS && e instanceof TypeElement) {
                    TypeElement t = (TypeElement) e;
                    isClass = true;
                    classAllName = t.getQualifiedName().toString();
                    element = t;
                    break;
                }
            }
            // If no comment is used on the class, it will return directly, return false to stop compilation
            if (!isClass) {
                return true;
            }
            // Return all nodes in the class
            List<? extends Element> enclosedElements = element.getEnclosedElements();
            // Save a collection of fields
            Map<TypeMirror, Name> fieldMap = new HashMap<>();
            for (Element ele : enclosedElements) {
                if (ele.getKind() == ElementKind.FIELD) {
                    //Type of field
                    TypeMirror typeMirror = ele.asType();
                    //Name of the field
                    Name simpleName = ele.getSimpleName();
                    fieldMap.put(typeMirror, simpleName);
                }
            }
            // Generate a Java source file
            JavaFileObject sourceFile = filer.createSourceFile(getClassName(classAllName));
            // Write code
            createSourceFile(classAllName, fieldMap, sourceFile.openWriter());
            // Manual compilation
            compile(sourceFile.toUri().getPath());
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR,e.getMessage());
        }
        messager.printMessage(Diagnostic.Kind.NOTE,"-----Auto generate source code complete");
        return true;
    }

    private void createSourceFile(String className, Map<TypeMirror, Name> fieldMap, Writer writer) throws IOException {
        // Generate source code
        JavaWriter jw = new JavaWriter(writer);
        jw.emitPackage(getPackage(className));
        jw.beginType(getClassName(className), "class", EnumSet.of(Modifier.PUBLIC));
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //field
            jw.emitField(type, name, EnumSet.of(Modifier.PRIVATE));
        }
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //getter
            jw.beginMethod(type, "get" + humpString(name), EnumSet.of(Modifier.PUBLIC))
                    .emitStatement("return " + name)
                    .endMethod();
            //setter
            jw.beginMethod("void", "set" + humpString(name), EnumSet.of(Modifier.PUBLIC), type, "arg")
                    .emitStatement("this." + name + " = arg")
                    .endMethod();
        }
        jw.endType().close();
    }

    /**
     * Compile file
     * @param path
     * @throws IOException
     */
    private void compile(String path) throws IOException {
        //Get the compiler
        JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
        //Document manager
        StandardJavaFileManager fileMgr =
                complier.getStandardFileManager(null, null, null);
        //get files
        Iterable units = fileMgr.getJavaFileObjects(path);
        //Compile task
        JavaCompiler.CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
        //Compile
        t.call();
        fileMgr.close();
    }

    /**
     * Hump naming
     *
     * @param name
     * @return
     */
    private String humpString(String name) {
        String result = name;
        if (name.length() == 1) {
            result = name.toUpperCase();
        }
        if (name.length() > 1) {
            result = name.substring(0, 1).toUpperCase() + name.substring(1);
        }
        return result;
    }

    /**
     * Read class name
     * @param name
     * @return
     */
    private String getClassName(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(name.lastIndexOf(".") + 1);
        }
        return result;
    }

    /**
     * Read package name
     * @param name
     * @return
     */
    private String getPackage(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(0, name.lastIndexOf("."));
        }else {
            result = "";
        }
        return result;
    }
}

In the custom annotation processor, the annotation explains the idea of each step in detail. First, read the annotated node, judge whether it is a class node, then generate a java source file, write java code using the javawriter framework, and compile the Java source file manually.

The javawriter framework is referenced as follows:

compile 'com.squareup:javawriter:2.5.1'

3.3 registration of annotation processor

The copyright of this article belongs to the official account of WeChat public code ID:onblog. If it is reproduced, please keep the original statement of the paragraph, and the offender will investigate it. If there are any shortcomings, please welcome the official account of WeChat public. ]

After coding, you also need to register the annotation processor with the javac compiler, so you need to provide a. jar file. Like other. jar files, you package your compiled annotation processor into this file. Also, in your. jar file, you have to package a special file javax.annotation.processing.Processor to META-INF/services directory. So your. jar file directory structure looks like this:

MyProcess.jar
    -com
        -example
            -MyProcess.class
    -META-INF
        -services
            -javax.annotation.processing.Processor

javax.annotation.processing The content of the. Processor file is a list, and each line is the full name of an annotation processor. For example:

com.example.MyProcess

In IDE, you only need to create a new meta-inf / services in the resources directory/ javax.annotation.processing . processor file.

Other registration methods

The previous registration method is very low-level, which is recommended by individuals. When there are too many annotation processors, this method is too cumbersome, so the other way is to use the framework of automatically registering annotation processors.

Add a reference to Google auto register annotation Library

implementation 'com.google.auto.service:auto-service:1.0-rc4'

Declared before annotation handler class

@AutoService(Processor.class)

4. Packing

At this time, we can use the project as jar package. Next, we will demonstrate the use process.

(1) Write a Demo.java

import cn.zyzpp.annotation.Data;

@Data
public class Demo {
    private String name;
    private double abc;
}

(2) Compile the java file in the Demo.java Open the console window under the folder, and remember to put the packaged jar package together in this directory.

javac -cp annotation-1.0-SNAPSHOT.jar Demo.java

(3) Using javap to view the compiled Demo.class

Compiled from "Demo.java"
public class Demo {
  public Demo();
  public double getAbc();
  public void setAbc(double);
  public java.lang.String getName();
  public void setName(java.lang.String);
}

Look again at this time Demo.java code

public class Demo {
  private double abc;
  private String name;
  public double getAbc() {
    return abc;
  }
  public void setAbc(double arg) {
    this.abc = arg;
  }
  public String getName() {
    return name;
  }
  public void setName(String arg) {
    this.name = arg;
  }
}

So far, we have officially developed a plug-in for automatically generating getter and setter methods. Some small partners may think that this is not very useful, and it can be done very easily with the IDE shortcut key. In fact, knowledge has been learned. What colorful framework can be made depends on the wisdom of our partners. For example, we will generally create a new Entity class, and then based on this new Dao layer, Service layer code. With the knowledge described in this article, we can create a code generator suitable for ourselves, saving time and improving development efficiency.

Bonus

Installation and use of IntelliJ IDEA lombok plug-in

supplement

It is better to rely on the processor as a Jar package. If the package fails, you can specify the version.

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

Copyright notice

The copyright of this article belongs to the official account of WeChat public code ID:onblog. If it is reproduced, please keep the original statement of the paragraph, and the offender will investigate it. If there are any shortcomings, please welcome the official account of WeChat public. ]

Posted by daedlus on Tue, 02 Jun 2020 22:56:22 -0700