By Wang Zaijun (Xifeng)
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?
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:
I hope you can write a simple Lombok tool after reading this article.
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.
The entire compilation process is roughly shown below:
Image from OpenJDK
1. Initialize the pluggable annotation processor
2. Parse and populate the symbol table
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
After learning about the theory above, let's go into practice. Modify AST and add your code.
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:
@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.
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.
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:
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.
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.
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.
Finally, let's look at how to run this tool correctly.
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)
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:
META-INFO.services
javax.annotation.processing.Processor
.com.study.practice.nameChecker.MySetterGetterProcessor
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:
The final class file is parsed as shown in the following figure:
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.
Introduction to treemarker:
http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html
Alibaba Cloud Partners with Korea Indie Game Association to Support Game Developer
1,057 posts | 259 followers
FollowAlibaba Cloud Community - October 28, 2022
mizhou - June 15, 2023
Alibaba Cloud Community - August 20, 2024
Alibaba Cloud Community - August 17, 2023
linear.zw - December 19, 2023
Alibaba Developer - March 5, 2020
1,057 posts | 259 followers
FollowA low-code development platform to make work easier
Learn MoreExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreMulti-source metrics are aggregated to monitor the status of your business and services in real time.
Learn MoreMore Posts by Alibaba Cloud Community