Lombok 学习及动手实现

Lombok 简介

Lombok 通过简单的注解形式帮助我们简化和消除一些繁琐但又必须存在的 Java 代码。简单来说,比如我们新建了一个类,并添加了一些字段,通常我们需要手动编写 getter 和 setter 方法、构造函数等。而 Lombok 的作用正是为了省去手动编写这些代码的麻烦,它能够在源码编译时自动为我们生成这些方法。

Lombok 的神奇之处在于,它让我们在源码中无需编写这些通用方法,但在生成的字节码文件中,这些方法却会被自动添加,从而极大提高了开发效率。

Lombck相关注解功能介绍

1. @Getter 和 @Setter

2. @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor

3. @ToString

4. @Data

5. @Builder

6. @Slf4j

7. @SneakyThrows

8. @Value

9. @Accessor

10. @Cleanup


Lombok 原理详解

它的核心原理是基于 编译期注解处理器(JSR 269)在代码编译时动态修改抽象语法树(AST)。Lombok 的注解处理器会扫描源代码中的注解,根据注解生成对应的代码,并将生成的代码插入到抽象语法树中。在 Java 编译过程中,源代码会被逐步转换为可执行的字节码文件(.class)。下面是具体的步骤:

编译流程的五个主要步骤

  1. 解析源代码

    • Javac(Java 编译器)会对源代码文件(.java)进行词法和语法解析,生成一棵 抽象语法树(AST)
    • AST 是源代码的树形表示形式,其中包含类、方法、变量等信息。
  2. 注解处理器的执行

    • 编译器调用 JSR 269 定义的注解处理器,扫描代码中的注解并对 AST 进行分析和修改。
    • 处理器可以生成新代码或修改现有的语法树。
  3. 语义分析和类型检查

    • 检查代码的类型、作用域是否正确。
    • 例如,是否调用了未定义的方法,变量的类型是否匹配等。
  4. 优化与中间代码生成

    • 将 AST 转换为中间表示,并进行代码优化。
  5. 生成字节码文件

    • 最终,Javac 根据修改后的 AST 生成 .class 字节码文件,供 JVM 执行。

编译过程可视化

1
2
3
4
5
6
7
8
9
10
11
PersonDTO.java

源文件解析并生成 AST 树

注解处理器(JSR 269):修改 AST

语义分析和类型检查

字节码生成

PersonDTO.class

使用@Data注解和手动编写代码实现Getter方法性能差别大吗?

在使用@Data注解和手动编写getter方法之间,性能上的差别实际上非常小甚至可以忽略不计。

编译时处理:

Lombok的@Data注解是在编译时通过注解处理器生成实际的字节码。当我们查看编译后的.class文件时,无论是使用Lombok还是手动编写,生成的字节码几乎是一样的。

运行时效率:

因为最终生成的字节码相同,运行时的性能是相同的。JVM执行的是字节码,因此无论是手动编写还是使用Lombok生成的方法,执行效率没有区别。

动手实现

我们实现一个简易版的 Lombok 自定义一个 Getter 方法,我们的实现步骤是:

  1. 自定义一个注解标签接口,并实现一个自定义的注解处理器;
  2. 利用 tools.jar 的 javac api 处理 AST (抽象语法树)
  3. 使用自定义的注解处理器编译代码。

自定义注解以及注解处理器

首先创建一个 MyGetter.java 自定义一个注解,代码如下:

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MyGetter { // 定义 Getter

}

再实现一个自定义的注解处理器: MyGetterProcessor.java,代码如下:

我们需要继承 AbstractProcessor 类,并重写其中的 init()process() 方法。在 process() 方法中,我们首先需要查询所有变量,然后为这些变量添加对应的方法。为此,可以使用 TreeMaker 对象和 Names 来处理 AST,如上述代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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;


@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.lombok.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {

private Messager messager; // 编译时期输入日志的
private JavacTrees javacTrees; // 提供了待处理的抽象语法树
private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
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) {
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class);
elementsAnnotatedWith.forEach(e -> {
JCTree tree = javacTrees.getTree(e);
tree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
// 在抽象树中找出所有的变量
for (JCTree jcTree : jcClassDecl.defs) {
if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
// 对于变量进行生成方法的操作
jcVariableDeclList.forEach(jcVariableDecl -> {
messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}

// 生成Getter方法
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
// 生成表达式 例如 this.a = a;
JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(
names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
statements.append(aThis);
JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

// 生成入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),
jcVariableDecl.getName(), jcVariableDecl.vartype, null);
List<JCTree.JCVariableDecl> parameters = List.of(param);

// 生成返回对象
JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(),
parameters, List.nil(), block, null);

}

/**
* 生成方法名
*
* @param name 变量名
* @return 方法名
*/
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}

/**
* 生成赋值语句
*
* @param lhs 左值
* @param rhs 右值
* @return 赋值语句
*/
private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
return treeMaker.Exec(
treeMaker.Assign(
lhs,
rhs
)
);
}
}

当这些代码写好之后,我们就可以新增一个 Person 类来试一下我们自定义的 @MyGetter 功能了,代码如下:

1
2
3
4
@MyGetter
public class Person {
private String name;
}

编译代码

将上面的三个文件(MyGetter.java, MyGetterProcessor.java, Person.java)放在同一个目录下,然后使用命令行编译这个 Person.java 文件,命令如下:

要在该文件的根目录下运行(即包含Person.java文件的目录),并且需要将tools.jar添加到classpath中。

  1. 编译 MyGetterProcessor.java, 也就是自定义的注解器:
    1
    javac -cp $JAVA_HOME/lib/tools.jar MyGetter* -d .

使用自定义注解器,编译 Person 类:

1
javac -processor com.example.lombok.MyGetterProcessor Person.java

编译成功后,我们可以看到生成的 Person.class 文件,然后我们使用 javap 命令查看生成的字节码文件,命令如下:

1
javap -p Person.class

注意,上面的代码需要安装一些依赖(在JDK 1.8的情况下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

通常情况下JDK自带Annotation Processing API
<dependency>
<groupId>com.sun.source</groupId>
<artifactId>javax.tools</artifactId>
<version>1.8.0</version>
</dependency>

<dependency>
<groupId>com.sun.source</groupId>
<artifactId>jsr269</artifactId>
<version>1.8.0</version>
</dependency>

参考链接:
https://juejin.cn/post/7282692088016601147?searchId=2025010413161885670941A6F85DCDCBD5#heading-7

https://juejin.cn/post/6844904106545381384?searchId=2025010413161885670941A6F85DCDCBD5

About this Post

This post is written by Rui Xu, licensed under CC BY-NC 4.0.

#Java