Lombok 简介
Lombok 通过简单的注解形式帮助我们简化和消除一些繁琐但又必须存在的 Java 代码。简单来说,比如我们新建了一个类,并添加了一些字段,通常我们需要手动编写 getter 和 setter 方法、构造函数等。而 Lombok 的作用正是为了省去手动编写这些代码的麻烦,它能够在源码编译时自动为我们生成这些方法。
Lombok 的神奇之处在于,它让我们在源码中无需编写这些通用方法,但在生成的字节码文件中,这些方法却会被自动添加,从而极大提高了开发效率。
Lombck相关注解功能介绍
1. @Getter 和 @Setter
- 功能:自动生成类中字段的getter和setter方法。
- 使用场景:
- 如果字段需要暴露访问方法,而无需手动编写这些方法。
- 可以通过
@Setter
的AccessLevel
属性限制setter方法的可见性(如private
)。
- 优点:减少冗余代码,增强代码可读性。
2. @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor
- 功能:
@NoArgsConstructor
:生成无参构造方法,可设置force
为true
以绕过final
字段初始化。
@AllArgsConstructor
:生成包含所有字段的构造方法。
@RequiredArgsConstructor
:生成仅包含final
字段和带@NonNull
注解字段的构造方法。
- 使用场景:
- 自动生成构造器,特别适用于DTO类、实体类。
- 结合Spring等框架时,方便依赖注入。
3. @ToString
- 功能:生成类的
toString
方法,便于调试和日志记录。
- 属性:
includeFieldNames
:是否包含字段名称。
exclude
:排除不希望包含在toString
中的字段。
callSuper
:是否调用父类的toString
方法。
- 使用场景:
- 需要快速实现
toString
方法且包含必要字段的信息。
4. @Data
- 功能:集合了
@Getter
、@Setter
、@ToString
、@EqualsAndHashCode
、@RequiredArgsConstructor
等常用注解。
- 使用场景:
- 适合需要快速实现POJO类(包括实体类或DTO)的场景。
- 注意:默认生成的
equals
和hashCode
方法以类的字段为比较标准。
5. @Builder
- 功能:为类生成Builder模式的实现,便于链式调用构造对象。
- 使用场景:
- 构造复杂对象时,例如需要多个字段或配置项。
- 增强代码可读性。
- 特性:
6. @Slf4j
- 功能:为类生成
org.slf4j.Logger
实例。
- 使用场景:
- 优点:无需手动声明和初始化
Logger
对象。
7. @SneakyThrows
- 功能:自动抛出受检异常,无需显式声明
throws
。
- 使用场景:
- 注意:可能隐藏异常处理逻辑,需谨慎使用。
8. @Value
- 功能:用于创建不可变对象,相当于
@Data
的不可变版本。
- 特点:
- 所有字段默认为
final
。
- 没有
setter
方法。
- 使用场景:
- 适合设计值对象(Value Object)时,例如枚举或常量类。
9. @Accessor
- 功能:定制getter和setter方法的命名规则。
- 使用场景:
10. @Cleanup
- 功能:为资源的自动释放提供支持,生成
try-finally
语句。
- 使用场景:
Lombok 原理详解
它的核心原理是基于 编译期注解处理器(JSR 269)在代码编译时动态修改抽象语法树(AST)。Lombok 的注解处理器会扫描源代码中的注解,根据注解生成对应的代码,并将生成的代码插入到抽象语法树中。在 Java 编译过程中,源代码会被逐步转换为可执行的字节码文件(.class
)。下面是具体的步骤:
编译流程的五个主要步骤
解析源代码:
- Javac(Java 编译器)会对源代码文件(
.java
)进行词法和语法解析,生成一棵 抽象语法树(AST)。
- AST 是源代码的树形表示形式,其中包含类、方法、变量等信息。
注解处理器的执行:
- 编译器调用 JSR 269 定义的注解处理器,扫描代码中的注解并对 AST 进行分析和修改。
- 处理器可以生成新代码或修改现有的语法树。
语义分析和类型检查:
- 检查代码的类型、作用域是否正确。
- 例如,是否调用了未定义的方法,变量的类型是否匹配等。
优化与中间代码生成:
生成字节码文件:
- 最终,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 方法,我们的实现步骤是:
- 自定义一个注解标签接口,并实现一个自定义的注解处理器;
- 利用 tools.jar 的 javac api 处理 AST (抽象语法树)
- 使用自定义的注解处理器编译代码。
自定义注解以及注解处理器
首先创建一个 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 {
}
|
再实现一个自定义的注解处理器: 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; 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; }
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) { ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>(); 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);
}
private Name getNewMethodName(Name name) { String s = name.toString(); return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length())); }
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中。
- 编译 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 命令查看生成的字节码文件,命令如下:
注意,上面的代码需要安装一些依赖(在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