×
Community Blog Do You Know the Principle of Lombok That Has Been Used for a Long Time?

Do You Know the Principle of Lombok That Has Been Used for a Long Time?

This article explains how to write a simple Lombok plug-in.

1

By Wang Zaijun (Xifeng)

Introduction

When writing Java code, I am tired of writing setter/getter methods. Since the Lombok plug-in debuted, I do not need to write those methods. Have you ever wondered how Lombok added setter/getter methods? Some said the introduction of Lombok in Java would pollute the dependency package. Can we write a tool to replace Lombok?

Related Points

  • Java Compilation Process
  • Understand the Lombok principle
  • Learn about the pluggable annotation processor

Analysis

The questions mentioned in the preface are the same question; how to obtain and modify Java source code.

First, we need to answer the following questions before answering the questions above:

  1. How does the Java compiler parse Java source code?
  2. What are the steps for the compiler to compile source code?
  3. How can we add content or perform code analysis when the compiler is working?

I hope you can write a simple Lombok tool after reading this article.

Answer

How to Parse Source Code

From the code to its compilation, there is a data structure called Abstract Syntax Tree (AST). You can view the picture below for the specific form. The data structure of AST is on the right.

2

What Are the Steps for Code Compilation?

The entire compilation process is roughly shown below:

3
Image from OpenJDK

1.  Initialize the pluggable annotation processor

2.  Parse and populate the symbol table

  • Lexical and Syntax Analysis: The character stream of the source code is converted into a tag set to construct an Abstract Syntax Tree.
  • Populate the Symbol Table: The symbol addresses and symbol information are generated.

3.  The Annotation Process of the Pluggable Annotation Processor: The execution phase of the pluggable annotation processor. Later, I will give two practical examples.

4.  Analysis and Bytecode Generation

  • Annotation Checking: Check the static information of the syntax
  • Data Flow and Control Flow Analysis: Check the dynamic running process of the program
  • Syntactic Sugar Parsing: Restore the syntactic sugar written in simplified code to its original form.
  • Bytecode Generation: Convert the information generated by the previous steps into bytecode.

After learning about the theory above, let's go into practice. Modify AST and add your code.

Practice

How to Implement a Tool to Add Setter/Getter Automatically

First, create an annotation:

@Retention(RetentionPolicy.SOURCE) // The Annotation is reserved only in the source code
@Target(ElementType.TYPE) // Used to modify the class
public @interface MySetterGetter {
}

Create an entity class that needs to generate the setter/getter method:

@MySetterGetter  // Add the annotation
public class Test {
    private String wzj;
}

Next, let's look at how to generate the string we want.

The overall code is listed below:

@SupportedAnnotationTypes("com.study.practice.nameChecker.MySetterGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MySetterGetterProcessor extends AbstractProcessor {
    // This is mainly about output information
    private Messager messager;
    private JavacTrees javacTrees;

    private TreeMaker treeMaker;
    private Names names;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.javacTrees = 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 the annotated classes
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetterGetter.class);
        elementsAnnotatedWith.forEach(element -> {
            // Obtain the AST structure of the class
            JCTree tree = javacTrees.getTree(element);
            // Traverse the class and modify the class
            tree.accept(new TreeTranslator(){
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // Find all variables in AST
                    for(JCTree jcTree: jcClassDecl.defs){
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)){
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl)jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    
                    // Perform operations on the generation method for variables
                    for (JCTree.JCVariableDecl jcVariableDecl : jcVariableDeclList) {
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));

                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    }


        // Generate a returned object
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());

        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewSetterMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null);
    }
    /**
     * Generate getter method
     * @param jcVariableDecl
     * @return
     */
    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // Generate an expression
        JCTree.JCReturn aReturn = treeMaker.Return(treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aReturn);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
        // No input parameter
        // Generate a returned object
        JCTree.JCExpression returnType = treeMaker.Type(jcVariableDecl.getType().type);
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewGetterMethodName(jcVariableDecl.getName()), returnType, List.nil(), List.nil(), List.nil(), block, null);
    }
    /**
     * Concatenate the Setter method name string
     * @param name
     * @return
     */
    private Name getNewSetterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
    }
    /**
     * Concatenate the Getter method name string
     * @param name
     * @return
     */
    private Name getNewGetterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
    }
    /**
     * Generate an expression
     * @param lhs
     * @param rhs
     * @return
     */
    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(lhs, rhs)
        );
    }
}

Since there are a lot of codes, let me explain them one by one:

The following is a brain diagram of the entire code structure. The following explanation will be based on this order:

4

A. Annotation

@SupportedAnnotationTypes indicates the annotation we need to listen to (such as @MySetterGetter), which we defined earlier.

@SupportedSourceVersion indicates what version of Java source code we want to process.

B. Parent Class

AbstractProcessor is the core class, and the compiler scans its subclasses when compiling. There is a core method achieved by subclass: public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv). If the system returns true, it means the AST structure of the compiled class has changed again, and lexical analysis and syntax analysis need to be performed again (please see the compilation flowchart mentioned above). If it returns false, there is no change in the AST structure of the compiled class.

C. Process Method

The main operating logic is listed below:

1.  Get all the classes annotated by MySetterGetter

2.  Traverse all classes and generate the AST structure of the class

3.  Perform operations on classes:

  • Find all variables in the class
  • Generate Set and Get methods for variables

4.  If the system returns true, it indicates that the class structure has changed and needs to be parsed again. If the system returns false, it indicates there is no change, and the class structure does not need to be parsed again.

D. Operate JCTree

It is mainly about the operation on the abstract tree. You can view the article in the attachment at the end of the article to learn more.

E. Method Name Concatenation

There is no difference from string concatenation. People that have used reflection should also know this operation.

That is all for the Lombok principle. It is very simple, right? Next, let's run and practice it.

F. Run

Finally, let's look at how to run this tool correctly.

1. Environment

The system environment is macOS Monterey. The Java version is listed below:

openjdk version "1.8.0_302"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_302-b08)
OpenJDK 64-Bit Server VM (Temurin)(build 25.302-b08, mixed mode)

2. Compile the Processor

Compile in the directory where you store MySetterGetter and MySetterGetterProcessor classes:

javac -cp $JAVA_HOME/lib/tools.jar MySetterGetter.java MySetterGetterProcessor.java

These three class files appear after the execution is successful:

5

3. Declare the Pluggable Annotation Processor

6

  • Create a package under the resources of your project named: META-INFO.services
  • Create a file named: javax.annotation.processing.Processor.
  • Fill in the address of your annotation processor. My configuration is: com.study.practice.nameChecker.MySetterGetterProcessor

4. Use Our Tools to Compile the Target Class

For example, we will compile the test.java this time. Its content is reviewed again:

@MySetterGetter  // Add the annotation
public class Test {
    private String wzj;
}

Then, we compile it. (Note: The path is in front of the class. You have to replace this with your engineering directory.*)

javac -processor com.study.practice.nameChecker.MySetterGetterProcessor com/study/practice/nameChecker/Test.java

After execution, if my code is not modified, these strings will be printed:

process 1
process 2
Note: wzj has been processed
process 1

Finally, the Test.class file is generated:

7

5. Achievements

The final class file is parsed as shown in the following figure:

8

When we see the Setter/Getter method, we are finished! Is it very simple?

So far, we have learned how to write a simple Lombok plug-in.

Appendix

Introduction to treemarker:

http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html

0 1 0
Share on

Alibaba Cloud Community

1,036 posts | 254 followers

You may also like

Comments