Writing in the front

I remember the first time I came into contact with Lombok when I was working on a new project. The first impression was not good. But it’s not bad after the whole team uses it. Said to save time, in the use of it seems to have no feeling, a bit like the space and TAB, are all at once, may be a habit problem. Getting back to the point, I’m more curious about how Lombok was implemented than debating whether it was used or not.

What is the Annotation Processor Tool

The full name of APT is Pluggable Annotation Processing API (Pluggable Annotation Processing API), which is a set of API provided by JSR269 specification. It can customize the compilation output we expect at compile time. APT cannot modify existing source code, it can only add new source code. Help us do some checking code, generate template code or configuration files etc. Common applications include Lombok, YML files with CTRL + left mouse button to locate specific files (annotation processor generates Spring-configuration-metadatad. json), Google AutoService (described below), etc.

Second, the operation process and basic principles

In terms of the overall structure of the Javac code, the compilation process can be roughly divided into one preparation process and three processing processes, as shown below.

  1. Preparation: Initialize the plug-in annotation handler.
  2. The process of parsing and populating symbol tables, including lexical and grammatical analysis. The character stream of the source code is transformed into a collection of tags, an abstract syntax tree is constructed, and the symbol table is filled. Generates symbolic addresses and symbolic information.
  3. Annotation processing by the plug-in annotation processor.
  4. Analysis and bytecode generation process, including annotation inspection, data flow and control flow analysis, decoding, bytecode generation.

If these plug-ins make changes to the syntax tree during annotation processing, the compiler will go back to parsing and populating the symbol table and reprocess it until no more changes have been made to the syntax tree by any plug-in annotation processor. Each loop is called a Round, as shown in the figure above

The above process corresponding to the code, Javac compile action entrance is com. Sun. View Javac. Main. JavaCompiler class, the above three process code logic focused on this class of the compile () and compile2 () method, The main processing of the whole compilation process is done by the 8 methods highlighted in the figure below

Customize Lombok’s project build

Maven-compiler-plugin: Set proc: None maven Annotation Processing Processor not found

<dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/.. /lib/tools.jar</systemPath>
        </dependency>
</dependencies>

<build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                    <executions>
                        <execution>
                            <id>default-compile</id>
                            <configuration>
                                <! Compile source files without comment handler -->
                                <compilerArgument>-proc:none</compilerArgument>
                                <source>1.8</source>
                                <target>1.8</target>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
</build>
Copy the code

Setter and Getter annotation handlers

1. The directory tree

├ ─ Java │ │ complie. Sh │ │ │ └ ─ com │ └ ─ hinotoyk │ ├ ─ jsr269 │ │ Getter. Java │ │ GetterProcessor. Java │ │ Setter. Java │ │ Java │ ├ ─ 0├ ─ sci-sci-imp. Java │ ├ ─ sci-sci-imp javax.annotation.processing.ProcessorCopy the code

2. Create Getter and Setter annotations

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
}
Copy the code
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}
Copy the code

3. Create annotation handlers for getters and setters

import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

// Get interested in getters
@SupportedAnnotationTypes("com.hinotoyk.jsr269.Getter")
// Support version, use 1.8 to write this
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {

    // Enter logs at compile time
    private Messager messager;

    // A tool to convert Element to JCTree, which provides an abstract syntax tree to be processed
    private JavacTrees trees;

    // encapsulates methods for creating AST nodes
    private TreeMaker treeMaker;

    // provides methods to create identifiers
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        // Get all elements marked by the @getter annotation (this element could be classes, variables, methods, and so on)
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);

        set.forEach(element -> {
            // Convert Element to JCTree
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                /*** * jctree. Visitor has a number of methods that we can override to get the desired information (from the method's parameters) : * For example, override visitClassDef to get the class information; * Override the visitMethodDef method to get the method information; * Rewrite the visitVarDef method to get the variable information; *@param jcClassDecl
                 */
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {

                    // Create a List of variable syntax tree nodes
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                    // Iterate over the defs, which is a detailed statement of class definition, including field, method definition, and so on
                    for (JCTree tree : jcClassDecl.defs) {
                        if(tree.getKind().equals(Tree.Kind.VARIABLE)) { JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree; jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl); }}// Generate a method for a variable
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, "get " + jcVariableDecl.getName() + " has been processed"); 
                        treeMaker.pos = jcVariableDecl.pos;
                        // Class to append the generated Getter
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl); }}); });// We have modified the AST, so return true
        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        * JCBlock: statement block syntax tree node * JCReturn: return statement syntax tree node * JCClassDecl: class definition syntax tree node * JCVariableDecl: Field/variable definition syntax tree nodes * JCMethodDecl: method definition syntax tree nodes * JCModifiers: access flag syntax tree nodes * JCExpression: expression syntax tree nodes, common subclasses are as follows * JCAssign: Assignment statement syntax tree node * JCIdent: Identifier syntax tree node, which can be variables, types, keywords, and so on */
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        
        statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC),//mods: access flags
                getNewMethodName(jcVariableDecl.getName()),//name: method name
                jcVariableDecl.vartype,//restype: Return type
                List.nil(),//typarams: generic parameter list
                List.nil(),//params: parameter list
                List.nil(),Thrown: List of exception declarations
                body,/ / the method body
                null);
    }

    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0.1).toUpperCase() + s.substring(1, name.length())); }}Copy the code

A Settter annotation handler is similar to a Getter, except that the steps to generate a set method are different. See OpenJDK-7 TreeMaker for details

import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("com.hinotoyk.jsr269.Setter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SetterProcessor extends AbstractProcessor {

    private Messager messager;

    private JavacTrees trees;

    private TreeMaker treeMaker;

    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Setter.class);

        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil(); 
                    for (JCTree tree : jcClassDecl.defs) {
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, "set " + jcVariableDecl.getName() + " has been processed");
                        treeMaker.pos = jcVariableDecl.pos;
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));

                    });
                    super.visitClassDef(jcClassDecl); }}); });return true;
    }


    private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(
                treeMaker.Exec(
                        treeMaker.Assign(
                                treeMaker.Select(
                                     treeMaker.Ident(names.fromString("this")),
                                     names.fromString(jcVariableDecl.name.toString())
                                ),
                                treeMaker.Ident(names.fromString(jcVariableDecl.name.toString()))
                        )
                )
        );

        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());

        // Generate input parameters
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(),jcVariableDecl.vartype, null);

        List<JCTree.JCVariableDecl> paramList = List.of(param);

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC), // The method limits the value
                setNewMethodName(jcVariableDecl.getName()), / / the method name
                treeMaker.Type(new Type.JCVoidType()), // Return type
                List.nil(),
                paramList, / / into the refs
                List.nil(),
                body,
                null
        );

    }

    private Name setNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0.1).toUpperCase() + s.substring(1, name.length())); }}Copy the code

4. Create a test Class

import com.hinotoyk.jsr269.Getter;
import com.hinotoyk.jsr269.Setter;

@Setter
@Getter
public class TestDemo {

    private String name;


    public static void main(String[] args) {
        TestDemo testDemo = new TestDemo();
        testDemo.setName("yk"); System.out.println(testDemo.getName()); }}Copy the code

5. Run

Running it directly is bound to generate an error, because this is actually supposed to be a separate project, an annotation processor project and a business project. We need to use the compiled annotation processor project to process other projects during compilation time. If we compile it together, we will definitely fail. We can write shell script as follows:

#! /usr/bin bash

if [ -d classes ]; then
    rm -rf classes;
fi
Create an output directory
mkdir classes

Compile the annotation processor-specific classes first, and -d specifies the output to the classes folder
javac -encoding UTF-8 -cp ${JAVA_HOME}/lib/tools.jar com/hinotoyk/jsr269/*.java -d classes/

-cp specifies the class path (fully qualified class name at runtime) -processor specifies the annotation class to compile the test class, and the class file is also specified to the classes folder
javac -cp classes -processor com.hinotoyk.jsr269.GetterProcessor,com.hinotoyk.jsr269.SetterProcessor com/hinotoyk/test/TestDemo.java -d classes/

#-cp specifies the class path and runs it
java -cp classes com.hinotoyk.test.TestDemo
Copy the code

You can clearly see that after executing the script, it runs normally, printing yk correctly

6. Packaging (SPI mechanism)

SPI (Service Provider Interface) is a built-in Service discovery mechanism in JDK. It is mainly used by developers of the framework, such as the java.sql.Driver interface, which can be implemented by database vendors. Of course, in order to let the system know the existence of specific implementation classes, it needs to use fixed storage rules. You need to create a file named after the service interface in the Meta-INF /services/ directory under Resources. The contents of this file are the implementation classes of the interface

The operation of packaging is relatively simple, Java has an SPI mechanism, including JDBC, Spring, Dubbo are used extensively. For example, you can see the jar package of Mysql connection, the corresponding directory has been found

Create a meta-INF folder under resources and a service folder below, Finally create a named javax.mail. The annotation. Processing. After the file Processor, write the fully qualified name in this file implementation class can, if multiple implementation classes, press enter branch to fill in

└ ─ resources └ ─ meta-inf └ ─ services └ ─ javax.mail. Annotation. Processing. The ProcessorCopy the code
com.hinotoyk.jsr269.GetterProcessor
com.hinotoyk.jsr269.SetterProcessor
Copy the code

Comment out the test class before packaging, otherwise the compiler will report an error

Packaging: the clean – > complie – > package – > install

Annotation processor SPI specific implementation can view the source code, the path is as follows: com.sun.tools.javac.main.JavaCompiler->compile()->this.initProcessAnnotations()-> this.procEnvImpl.setProcessors()->this.initProcessorIterator()->JavacProcessingEnvironment.ServiceIterator()

5. Use custom Lombok for engineering projects

1. Create a New Maven project and introduce dependencies

<dependency>
            <groupId>com.hinotoyk.jsr269</groupId>
            <artifactId>jsr269Demo</artifactId>
            <version>1.0 the SNAPSHOT</version>
</dependency>
Copy the code

2. Create and run Demo

package com.hinotoyk.nichijou.Test;

import com.hinotoyk.jsr269.Getter;
import com.hinotoyk.jsr269.Setter;

@Getter
@Setter
public class Demo {

    private String name;

    public Demo(a){}public static void main(String[] args) {
        Demo a = new Demo();
        a.setName("ykkkkkk"); System.out.println(a.getName()); }}Copy the code

Run directly, as shown in the figure

Vi. Introduction: Use of AutoService

AutoService’s Github and Readme have tutorials

1. Delete the mate-INF folder and modify the POM file

Delete the mate-INF folder we created above

Add corresponding dependencies and modify maven’s compiled plug-in


      

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hinotoyk.jsr269</groupId>
    <artifactId>jsr269Demo</artifactId>
    <version>1.0 the SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>jsr269Demo</name>
    <! -- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/.. /lib/tools.jar</systemPath>
        </dependency>

        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service-annotations</artifactId>
            <version>1.0-rc7</version>
        </dependency>
    </dependencies>

    <build>
       <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                    <configuration>
                        <annotationProcessorPaths>
                            <path>
                                <groupId>com.google.auto.service</groupId>
                                <artifactId>auto-service</artifactId>
                                <version>1.0-rc7</version>
                            </path>
                        </annotationProcessorPaths>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
Copy the code

2. Add an autoService annotation

Add @AutoService(Processor.class) to both SetterProcessor and getterProcessor classes

Pack 3.

Clean ->complie->package->install

Let’s look at the compiled output, which is actually generated automatically by the annotation processor for us

Write in the last

This article finally touch out, when writing this article to understand a lot of knowledge points, but also including the knowledge of the compiler did not contact before, looked at a lot of technical masters of the article, also read a rough “in-depth understanding of Java virtual machine” inside the relevant chapter, income bandits shallow.

The above.

The resources

JVM series 6 (custom plug-in annotator) Javac command first look at Java annotation processor — modify syntax tree at compile time Advanced JVM Features and Best Practices (3rd edition)