Compile time annotation

I learned to use compile time annotations to generate new files a long time ago, such as the butter knife in Android, but my goal is not to generate new classes, but to modify bytecode at compile time and make plug-ins like Lombok. Recently, I took time to find some materials and finally wrote my own compile time bytecode modification annotations

reference resources: https://liuyehcf.github.io/2018/02/02/Java-JSR-269-%E6%8F%92%E5%85%A5%E5%BC%8F%E6%B3%A8%E8%A7%A3%E5%A4%84%E7%90%86%E5%99%A8/
I wrote about the corresponding mavan configuration earlier. There are a lot on the Internet, so I won't repeat it here
Note that before writing, you must first introduce the jdk's own tools Jar file

<dependencies>
        <dependency>
            <groupId>jdk.tools</groupId>
            <artifactId>jdk.tools</artifactId>
            <version>${java.version}</version>
            <scope>system</scope>
            <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

NoArgsConstructorProcessor

@SupportedAnnotationTypes("annotations.NoArgsConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NoArgsConstructorProcessor extends AbstractProcessor {

    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        javacTrees=JavacTrees.instance(context);//Convert Element to JCTree
        treeMaker=TreeMaker.instance(context);// Construct syntax tree node
        names=Names.instance(context);// Used to generate identifiers
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(NoArgsConstructor.class);
        elements.forEach((Consumer<Element>) element -> javacTrees.getTree(element).accept(new TreeTranslator(){
            @Override//Use visitor mode to access the definition of the class
            public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                super.visitClassDef(jcClassDecl);
                if(!hasNoArgsConstructor(jcClassDecl)){
                    // Note that the List returns a new object each time
                    jcClassDecl.defs=jcClassDecl.defs.append(createNoArgsConstructor(jcClassDecl));
                }
            }
        }));
        return true;
    }

    private JCTree.JCMethodDecl createNoArgsConstructor(JCTree.JCClassDecl jcClassDecl) {
        treeMaker.pos=jcClassDecl.pos;//Note that the offset must be aligned
        JCTree.JCBlock block=treeMaker.Block(0, List.nil());//Empty code block
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),//Access identity
                names.fromString("<init>"),//Method name
                treeMaker.TypeIdent(TypeTag.VOID),//return type
                List.nil(),//Generic list
                List.nil(),//parameter list
                List.nil(),//Exception thrown
                block,//Code block
                null);
    }

    // Determine whether there is a default null parameter constructor
    private boolean hasNoArgsConstructor(JCTree.JCClassDecl jcClassDecl) {
        for (JCTree jcTree:jcClassDecl.defs){
            if(jcTree.getKind().equals(Tree.Kind.METHOD)){
                JCTree.JCMethodDecl methodDecl= (JCTree.JCMethodDecl) jcTree;
                if(methodDecl.getName().toString().equals("<init>")&&methodDecl.getParameters().isEmpty()){
                    return true;
                }
            }
        }
        return false;
    }

}

AllArgsConstructorProcessor

@SupportedAnnotationTypes("annotations.AllArgsConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AllArgsConstructorProcessor extends AbstractProcessor {

    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AllArgsConstructor.class);
        elements.forEach(new Consumer<Element>() {
            @Override
            public void accept(Element element) {
                javacTrees.getTree(element).accept(new TreeTranslator(){
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                        super.visitClassDef(jcClassDecl);
                        jcClassDecl.defs=jcClassDecl.defs.append(createAllArgsConstructor(jcClassDecl));
                    }
                });
            }
        });
        return true;
    }

    private JCTree.JCMethodDecl createAllArgsConstructor(JCTree.JCClassDecl jcClassDecl) {
        treeMaker.pos=jcClassDecl.pos;
        List<JCTree.JCVariableDecl> param=getParam(jcClassDecl);
        // ListBuffer is similar to Java Native list. You can directly use append or use toList to convert to tools List under jar
        ListBuffer<JCTree.JCStatement> buffer=new ListBuffer<>();
        param.forEach(variableDecl -> buffer.append(
                treeMaker.Exec(treeMaker.Assign(//Assignment operation
                treeMaker.Select(treeMaker.Ident(names.fromString("this")),variableDecl.name),// this.<name>
                treeMaker.Ident(variableDecl.name)))));//<param.name>
        JCTree.JCBlock block = treeMaker.Block(0, buffer.toList());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),names.fromString("<init>"),
                treeMaker.TypeIdent(TypeTag.VOID), List.nil(),param, List.nil(),block,null);
    }

    // Get all non static Final variables
    private List<JCTree.JCVariableDecl> getParam(JCTree.JCClassDecl jcClassDecl) {
        ListBuffer<JCTree.JCVariableDecl> buffer = new ListBuffer<>();
        for (JCTree tree:jcClassDecl.defs){
            if(tree.getKind().equals(Tree.Kind.VARIABLE)){
                JCTree.JCVariableDecl variableDecl= (JCTree.JCVariableDecl) tree;
                Set<Modifier> flags = variableDecl.mods.getFlags();
                if(!flags.contains(Modifier.FINAL)&&!flags.contains(Modifier.STATIC)){
                    buffer.append(treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),variableDecl.name,
                            variableDecl.vartype,null));
                }
            }
        }
        return buffer.toList();
    }

}

DataProcessor

@SupportedAnnotationTypes("annotations.Data")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataProcessor extends AbstractProcessor {

    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getElementsAnnotatedWith(Data.class).forEach(new Consumer<Element>() {
            @Override
            public void accept(Element element) {
                javacTrees.getTree(element).accept(new TreeTranslator(){
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                        super.visitClassDef(jcClassDecl);
                        List<JCTree.JCVariableDecl> param=getParam(jcClassDecl);
                        param.forEach(variableDecl -> {
                            //  Add getter setter
                            jcClassDecl.defs=jcClassDecl.defs.append(createGetter(variableDecl));
                            jcClassDecl.defs=jcClassDecl.defs.append(createSetter(variableDecl));
                        });
                    }
                });
            }
        });
        return true;
    }

    private JCTree.JCMethodDecl createSetter(JCTree.JCVariableDecl variableDecl) {
        treeMaker.pos=variableDecl.pos;
        JCTree.JCBlock block = treeMaker.Block(0, List.of(treeMaker.Exec(treeMaker.Assign(
                treeMaker.Select(treeMaker.Ident(names.fromString("this")), variableDecl.name),
                treeMaker.Ident(variableDecl.name)))));
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
                names.fromString(toSetter(variableDecl.getName().toString())),treeMaker.TypeIdent(TypeTag.VOID),
                List.nil(),List.of(variableDecl),List.nil(),block,null);
    }

    private String toSetter(String name) {
        return "set"+name.substring(0,1).toUpperCase()+name.substring(1);
    }

    private JCTree.JCMethodDecl createGetter(JCTree.JCVariableDecl variableDecl) {
        treeMaker.pos=variableDecl.pos;
        JCTree.JCStatement statement=treeMaker.Return(// return
                treeMaker.Select(treeMaker.Ident(names.fromString("this")),variableDecl.name));// this.<name>
        JCTree.JCBlock block = treeMaker.Block(0, List.of(statement));
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
                names.fromString(toGetter(variableDecl.getName().toString())),variableDecl.vartype,List.nil(),
                List.nil(),List.nil(),block,null);
    }

    private String toGetter(String name) {
        return "get"+name.substring(0,1).toUpperCase()+name.substring(1);
    }

    private List<JCTree.JCVariableDecl> getParam(JCTree.JCClassDecl jcClassDecl) {
        ListBuffer<JCTree.JCVariableDecl> buffer = new ListBuffer<>();
        for (JCTree tree:jcClassDecl.defs){
            if(tree.getKind().equals(Tree.Kind.VARIABLE)){
                JCTree.JCVariableDecl variableDecl= (JCTree.JCVariableDecl) tree;
                Set<Modifier> flags = variableDecl.mods.getFlags();
                if(!flags.contains(Modifier.FINAL)&&!flags.contains(Modifier.STATIC)){
                    buffer.append(treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),variableDecl.name,
                            variableDecl.vartype,null));
                }
            }
        }
        return buffer.toList();
    }

}

Study notes

Document address

https://liuyehcf.github.io/2018/02/02/Java-JSR-269-%E6%8F%92%E5%85%A5%E5%BC%8F%E6%B3%A8%E8%A7%A3%E5%A4%84%E7%90%86%E5%99%A8/

prepare

You must import jdk's tools Jar file

<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>${java.version}</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

Add packaging plug-ins to prevent dead circulation

<build>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <!-- Disable annotation processing for ourselves.-->
                <compilerArgument>-proc:none</compilerArgument>
            </configuration>
        </plugin>
    </plugins>
</build>

Javax under MATE-INF/services annotation. processing. Processor register annotation processor

Basic concepts

Java compilation process

Parsing and filling symbol table process

Annotation processing process of plug-in annotation processor

Analysis and bytecode generation process

JCTree

The base class and the attribute * * pos * * specify the position in the syntax tree. Therefore, each syntax component cannot be new directly and needs to be generated using TreeMake

Common subclasses

  1. JCStatement: declare syntax tree nodes. Common subclasses are as follows
    • JCBlock: statement block syntax tree node
    • JCReturn: return statement syntax tree node
    • JCClassDecl: class definition syntax tree node
    • JCVariableDecl: field / variable definition syntax tree node
  2. JCMethodDecl: method definition syntax tree node
  3. JCModifiers: access flag syntax tree node
  4. JCExpression: expression syntax tree node. Common subclasses are as follows
    • JCAssign: assignment statement syntax tree node
    • JCIdent: identifier syntax tree node, which can be variable, type, keyword, etc

TreeMaker

It is used to create syntax tree nodes. pos will be set for the created nodes. Context sensitive new cannot be used directly

TreeMaker.Modifiers

Access identifier flags: access flag annotations: annotation list

TreeMaker.ClassDef

Used to define class mods: access flag name: class name typarams: generic parameter list extending: parent class implementing: interface list defs: detailed statement of class definition, including field, method definition, etc

TreeMaker.MethodDef

mods: access flag name: method name restype: return type typeparams: generic parameter list params: parameter list throw: exception declaration list body: method body defaultValue: default method (it may be the default in the interface) m: Method symbol mtype: method type. It contains multiple types, including generic parameter type, method parameter type, exception parameter type and return parameter type

TreeMaker.VarDef

Used to create a field or variable mods: access flag vartype: type init: initialization statement v: variable symbol

TreeMaker.Ident

Used to create an identifier

TreeMaker.Return

Used to create a return statement

TreeMaker.Select

Get object property selected: The expression to the left of the operator selector: The name to the right of the operator

TreeMaker.NewClass

encl for creating new syntax tree: I don't quite understand the meaning of this parameter typeargs: parameter type list clazz: type of object to be created args: parameter list def: class definition

TreeMaker.Apply

Method call typeargs: parameter type list fn: call statement args: parameter list

TreeMaker.Assign

Create assignment statement lhs: expression on the left of assignment statement rhs: expression on the right of assignment statement

TreeMaker.Exec

JCExpressionStatement for executing executable syntax tree

TreeMaker.Block

Used to create the syntax tree node of combined statements flags: access flag stats: statement list

com.sun.tools.javac.util.List

A special List returns a new List each time an object is added

com.sun.tools.javac.util.ListBuffer

It is similar to Java Native list and provides a method to convert to list

example

Create method

treeMaker.MethodDef(
    treeMaker.Modifiers(Flags.PUBLIC), //Access flag
    names.fromString("<init>"), //name
    treeMaker.TypeIdent(TypeTag.VOID), //Return type
    List.nil(), //Generic parameter list
    List.nil(), //parameter list
    List.nil(), //Exception list
    jcBlock, //Method body
    null //Default method (may be the default in the interface)
);

Method parameters

treeMaker.VarDef(
    treeMaker.Modifiers(Flags.PARAMETER), //Access flag. Extremely pit Dad!!!
    prototypeJCVariable.name, //name
    prototypeJCVariable.vartype, //type
    null //Initialization statement
);

Assignment statement

treeMaker.Exec(
    treeMaker.Assign(
        treeMaker.Select(
            treeMaker.Ident(names.fromString(THIS)),
            jcVariable.name
        ),
        treeMaker.Ident(jcVariable.name)
    )
)

new object

treeMaker.NewClass(
    null, //The meaning is unclear
    List.nil(), //Generic parameter list
    treeMaker.Ident(builderClassName), //Class name created
    List.nil(), //parameter list
    null //Class definition is estimated to be used to create anonymous inner classes
)

Method call

treeMaker.Exec(
    treeMaker.Apply(
        List.nil(),
        treeMaker.Select(
            treeMaker.Ident(getNameFromString(IDENTIFIER_DATA)),
            jcMethodDecl.getName()
        ),
        List.of(treeMaker.Ident(jcVariableDecl.getName())) //Incoming parameter collection
    )
)

extend

Java compiler compilation process

Parser: map the symbol stream to an AST

Enter: find all definitions of the current range and convert to symbols

Process Annotations: optional. It processes compile time annotations. Reflection operations are not allowed

Attribute: adds an attribute to the generated AST

Flow: check the data flow

Desugar: reducing sugar

Generate: generate class files

Keywords: Java compiler processing javac annotations

Added by jtravis on Wed, 29 Dec 2021 15:21:46 +0200