Lombok (注解插件)
# Lombok使用
前言
在 Java 开发中,我们经常需要编写大量重复的“样板代码”(Boilerplate Code),例如 JavaBean 的 getter/setter 方法、构造函数、toString()
、equals()
和 hashCode()
方法,以及资源关闭、日志对象创建等。这些代码虽然必要,但编写和维护它们既枯燥又容易出错。Lombok 应运而生,它是一个强大的 Java 库,通过在编译期利用注解处理器 (Annotation Processor) 自动生成这些样板代码,极大地简化了代码,让开发者能够更专注于核心业务逻辑,显著提升开发效率和代码可读性。本文将带你全面深入地了解 Lombok 的安装配置、核心注解使用、常见问题及进阶技巧。
# 一、Lombok 的安装与配置
在项目中使用 Lombok,需要两步关键配置:添加项目依赖和安装 IDE 插件。
🔍 1.1 Lombok 是什么?
Lombok 本质上是一个 Java 注解处理器。它在 Java 代码编译阶段介入,根据你在源代码中使用的 Lombok 注解(如 @Getter
, @Data
等),自动生成对应的 Java 代码(如 getter/setter 方法等),并将这些生成的代码注入到编译后的 .class
文件中。因此,你的源代码可以保持简洁,而最终运行的字节码包含了所有必要的样板代码。
✅ 1.2 添加项目依赖
你需要将 Lombok 库添加到项目的构建依赖中,以便编译器在编译时能够找到并使用 Lombok 的注解处理器。
Maven 项目配置 (pom.xml
)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- 强烈建议使用官方发布的最新的稳定版本 -->
<version>1.18.32</version>
<!--
scope 设置为 provided 非常重要!
这表示 Lombok 库仅在编译和测试阶段需要,
Lombok 生成的代码会直接编译进 .class 文件,
最终运行的应用并不需要 lombok.jar 这个依赖,
因此不应将其打包到最终的应用程序(如 WAR 或 JAR)中。
-->
<scope>provided</scope>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
Gradle 项目配置 (build.gradle
/ build.gradle.kts
)
// build.gradle (Groovy DSL)
dependencies {
// 编译时依赖:仅在编译 Java 源代码时需要
compileOnly 'org.projectlombok:lombok:1.18.32'
// 注解处理器:告诉 Gradle 使用 Lombok 作为注解处理器
annotationProcessor 'org.projectlombok:lombok:1.18.32'
// 如果是测试代码也需要 Lombok(例如测试类使用了 @Data)
testCompileOnly 'org.projectlombok:lombok:1.18.32'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.32'
}
// build.gradle.kts (Kotlin DSL)
// dependencies {
// compileOnly("org.projectlombok:lombok:1.18.32")
// annotationProcessor("org.projectlombok:lombok:1.18.32")
//
// testCompileOnly("org.projectlombok:lombok:1.18.32")
// testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
✅ 1.3 安装 IDE 插件(!!! 必不可少 !!!)
仅仅添加项目依赖是不够的!因为 Lombok 是在编译期生成代码,你的集成开发环境 (IDE) 如果不识别 Lombok 注解,就无法正确地提示、导航、重构那些由 Lombok 生成的方法或字段,甚至会显示错误。因此,必须为你的 IDE 安装 Lombok 插件。
IntelliJ IDEA 配置:
- 安装 Lombok 插件: 打开
File
→Settings
(或Preferences
on macOS) →Plugins
。在 Marketplace 中搜索 "Lombok" 并安装。安装后可能需要重启 IDEA。 - 启用注解处理: 打开
File
→Settings
(或Preferences
) →Build, Execution, Deployment
→Compiler
→Annotation Processors
。确保勾选了 "Enable annotation processing"。对于较新版本的 IDEA,这一步通常在安装插件后会自动完成或不再需要手动配置。
- 安装 Lombok 插件: 打开
Eclipse 配置:
- 下载
lombok.jar
: 从 Lombok 官网 (opens new window) 或 Maven 仓库下载对应版本的lombok.jar
文件。 - 运行安装程序: 关闭 Eclipse。在命令行中运行
java -jar lombok.jar
。这会启动一个图形化安装界面。 - 指定 Eclipse 安装位置: 在安装界面中,它通常会自动检测到你的 Eclipse 安装。如果没有,手动指定 Eclipse 的安装目录。
- 点击 "Install / Update": 完成安装。
- 重启 Eclipse: 重新启动 Eclipse,Lombok 插件即可生效。
- 下载
验证安装: 安装完成后,在 IDE 中创建一个简单的类,使用 @Data
或 @Getter
/@Setter
注解,IDE 应该不再报错,并且能够通过代码提示或结构视图看到生成的 getter/setter 等方法。
# 二、Lombok 核心注解详解
Lombok 提供了丰富的注解来消除不同类型的样板代码。
# 2.1 @Getter
与 @Setter
:告别手动 getter/setter
这两个是最基础也是最常用的注解,用于自动为类中的字段生成标准的 getter 和 setter 方法。它们可以应用在类级别(为所有非静态字段生成)或字段级别(仅为特定字段生成)。
import lombok.Getter;
import lombok.Setter;
import lombok.AccessLevel; // 用于指定访问级别
/**
* 使用 @Getter 和 @Setter 的示例类
*/
@Getter // 应用于类级别:为本类所有非静态字段生成 public getter 方法
@Setter // 应用于类级别:为本类所有非非 final 非静态字段生成 public setter 方法
public class UserProfile {
private String userId; // 生成 public getUserId() 和 public setUserId(String userId)
private String displayName; // 生成 public getDisplayName() 和 public setDisplayName(String displayName)
@Setter(AccessLevel.PROTECTED) // 应用于字段级别:仅为 email 生成 protected setter
private String email; // 生成 public getEmail() 和 protected setEmail(String email)
// 对于 final 字段,@Setter 会被忽略,但 @Getter 仍然有效
private final String registrationDate; // 生成 public getRegistrationDate(),没有 setter
// 静态字段会被忽略
private static final String DEFAULT_ROLE = "GUEST";
// 特殊用法:懒加载 getter
// 仅在第一次调用 getFullProfile() 时才执行 calculateFullProfile() 并缓存结果
@Getter(lazy = true)
private final String fullProfile = calculateFullProfile();
// 用于懒加载计算的私有方法
private String calculateFullProfile() {
System.out.println("Calculating full profile for " + userId + "...");
// 模拟复杂的计算过程
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
return userId + " | " + displayName + " | Registered on " + registrationDate;
}
// 手动提供一个构造函数来初始化 final 字段
public UserProfile(String userId, String displayName, String email, String registrationDate) {
this.userId = userId;
this.displayName = displayName;
this.email = email;
this.registrationDate = registrationDate;
}
}
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
Lombok 生成的代码(示意):
public class UserProfile {
// ... 原有字段 ...
// --- 生成的 Getter ---
public String getUserId() { return this.userId; }
public String getDisplayName() { return this.displayName; }
public String getEmail() { return this.email; }
public String getRegistrationDate() { return this.registrationDate; }
// --- 生成的 Setter ---
public void setUserId(String userId) { this.userId = userId; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
protected void setEmail(String email) { this.email = email; } // 注意访问级别
// --- 懒加载 Getter 的实现 (简化版,实际更复杂以保证线程安全) ---
private volatile String fullProfileCache; // 缓存字段
private final Object fullProfileLock = new Object(); // 锁对象
public String getFullProfile() {
if (this.fullProfileCache == null) { // 第一次检查(无锁)
synchronized(this.fullProfileLock) { // 加锁
if (this.fullProfileCache == null) { // 第二次检查(有锁)
this.fullProfileCache = calculateFullProfile(); // 计算并缓存
}
}
}
return this.fullProfileCache;
}
// ... 原有方法和构造函数 ...
}
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
常用配置参数:
value
(或直接写级别):AccessLevel.PUBLIC
(默认),PROTECTED
,PACKAGE
,PRIVATE
,NONE
(不生成)。例如@Getter(AccessLevel.PROTECTED)
。lazy
:boolean
类型,默认为false
。设为true
时,为final
字段生成线程安全的懒加载 getter。
# 2.2 @ToString
:智能生成 toString()
自动生成 toString()
方法的实现,默认包含所有非静态字段。可以方便地定制包含或排除哪些字段,以及是否调用父类的 toString()
。
import lombok.ToString;
/**
* 使用 @ToString 注解的示例类
*/
@ToString(
// exclude 可以在类级别排除字段,通常用于密码、大对象等敏感或不重要信息
exclude = {"internalCache", "password"},
// callSuper=true 会在 toString() 结果中包含父类的 toString() 输出
callSuper = true,
// includeFieldNames=false 则只输出字段值,不输出字段名
includeFieldNames = true // 默认为 true
)
public class ServerConfig extends BaseConfig { // 假设继承自 BaseConfig
private String ipAddress;
private int port;
// 使用 @ToString.Exclude 在字段级别排除
@ToString.Exclude
private String password; // 此字段不会出现在 toString() 结果中
private Object internalCache; // 也被类级别的 exclude 排除了
// 使用 @ToString.Include 可以强制包含某个字段,即使它默认可能不被包含(如静态字段)
// 也可以用来修改字段在 toString() 中的名称
@ToString.Include(name = "Max Connections", rank = 1) // rank 用于控制输出顺序,数字越小越靠前
private int maxConnections = 100;
private static final String CONFIG_TYPE = "Server"; // 静态字段默认不包含
public ServerConfig(String ipAddress, int port, String password) {
super("ServerBase"); // 调用父类构造
this.ipAddress = ipAddress;
this.port = port;
this.password = password;
}
}
// 假设的父类
@ToString
class BaseConfig {
private String configName;
public BaseConfig(String name) { this.configName = name; }
}
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
Lombok 生成的 toString()
代码(示意):
@Override
public String toString() {
// 因为 callSuper = true,先调用父类的 toString()
// 因为 includeFieldNames = true,包含字段名
// password 和 internalCache 被排除了
// maxConnections 被包含并重命名了
return "ServerConfig(super=" + super.toString() + // 调用父类 toString()
", ipAddress=" + this.ipAddress +
", port=" + this.port +
", Max Connections=" + this.maxConnections + // 自定义名称和顺序
")";
}
2
3
4
5
6
7
8
9
10
11
12
常用配置参数:
exclude
:String[]
,指定要从toString()
中排除的字段名。of
:String[]
,只包含指定的字段名,与exclude
互斥。callSuper
:boolean
,是否在输出中调用父类的toString()
方法,默认为false
。includeFieldNames
:boolean
,是否在输出中包含字段名,默认为true
。doNotUseGetters
:boolean
,生成toString
时直接访问字段而不是调用 getter 方法,默认为false
。
# 2.3 @EqualsAndHashCode
:安全生成 equals()
和 hashCode()
自动生成 equals(Object other)
和 hashCode()
方法。默认使用所有非静态、非 transient 字段。可以精确控制哪些字段参与比较。
重要: 正确实现 equals()
和 hashCode()
对于 Set
、Map
等集合的正确工作至关重要。Lombok 可以帮助避免手动实现时容易犯的错误(例如忘记更新 hashCode
)。
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* 使用 @EqualsAndHashCode 注解的示例类
*/
@Getter
@EqualsAndHashCode(
// exclude 可以在类级别排除字段
exclude = {"lastLoginTime", "transientData"},
// callSuper=true 会在比较时考虑父类的字段 (如果父类也正确实现了 equals/hashCode)
callSuper = false // 默认为 false,除非继承自非 Object 类且需要比较父类字段
// onlyExplicitlyIncluded = true 时,只有用 @EqualsAndHashCode.Include 标记的字段才会参与比较
// onlyExplicitlyIncluded = false (默认)
)
public class ProductKey {
private final String category; // final 字段默认参与比较
private final String productCode; // final 字段默认参与比较
private transient Object transientData; // transient 字段默认不参与比较
private java.util.Date lastLoginTime; // 被 exclude 排除
// 使用 @EqualsAndHashCode.Include 强制包含某个字段或方法的结果
// @EqualsAndHashCode.Include
// public String getNormalizedCode() { return productCode.toUpperCase(); }
// 使用 @EqualsAndHashCode.Exclude 在字段级别排除
@EqualsAndHashCode.Exclude
private String description; // 此字段不参与比较
public ProductKey(String category, String productCode) {
this.category = category;
this.productCode = productCode;
}
// Lombok 会生成一个 canEqual 方法,用于配合 equals 实现,确保比较的是相同类型的对象
// protected boolean canEqual(Object other) { ... }
}
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
Lombok 生成的代码(示意):
@Override
public boolean equals(final Object o) {
if (o == this) return true; // 同一对象
if (!(o instanceof ProductKey)) return false; // 类型不同
final ProductKey other = (ProductKey) o;
if (!other.canEqual((Object)this)) return false; // 确保子类比较正确 (Lombok 生成 canEqual)
// 比较参与计算的字段 (category 和 productCode)
final Object this$category = this.getCategory();
final Object other$category = other.getCategory();
if (this$category == null ? other$category != null : !this$category.equals(other$category)) return false;
final Object this$productCode = this.getProductCode();
final Object other$productCode = other.getProductCode();
if (this$productCode == null ? other$productCode != null : !this$productCode.equals(other$productCode)) return false;
// 如果 callSuper=true,还会调用 super.equals(o)
return true; // 所有参与比较的字段都相等
}
// Lombok 生成的 canEqual 方法
protected boolean canEqual(final Object other) {
return other instanceof ProductKey;
}
@Override
public int hashCode() {
final int PRIME = 59; // 一个素数
int result = 1; // 初始值
// 计算参与字段的哈希码
final Object $category = this.getCategory();
result = result * PRIME + ($category == null ? 43 : $category.hashCode()); // 43 是对 null 的约定哈希码
final Object $productCode = this.getProductCode();
result = result * PRIME + ($productCode == null ? 43 : $productCode.hashCode());
// 如果 callSuper=true,还会加上 super.hashCode()
return result;
}
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
常用配置参数:
exclude
:String[]
,排除指定字段。of
:String[]
,只使用指定字段。callSuper
:boolean
,是否调用父类的equals
和hashCode
,默认为false
。当你继承的类不是Object
且需要考虑父类字段时设为true
。onlyExplicitlyIncluded
:boolean
,设为true
时,只有被@EqualsAndHashCode.Include
标记的成员才参与计算。doNotUseGetters
:boolean
,直接访问字段而非 getter,默认为false
。
# 2.4 构造器相关注解
Lombok 提供了一系列注解来自动生成不同类型的构造器。
# 2.4.1 @NoArgsConstructor
生成一个无参数的构造器。如果类中有 final
字段,会导致编译错误,除非使用 @NoArgsConstructor(force = true)
,但这会用 0
/false
/null
初始化 final
字段,可能不是你想要的。
import lombok.NoArgsConstructor;
import lombok.AccessLevel;
/**
* 使用 @NoArgsConstructor 生成无参构造器
*/
@NoArgsConstructor(
// access 可以指定生成的构造器的访问级别
access = AccessLevel.PUBLIC, // 默认是 public
// force=true 会强制生成无参构造器,并将 final 字段初始化为 0/false/null
// 通常不推荐,除非明确知道后果或配合其他机制(如反序列化)
force = false
)
public class ApiConfig {
private String apiKey;
private String apiSecret;
private int timeout = 60; // 可以有默认值
// 如果有 final 字段且 force=false,会编译失败
// private final String endpoint = "https://api.example.com";
}
// 生成的代码: public ApiConfig() {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用途: JavaBean 规范、JPA 实体、反序列化框架等通常需要无参构造器。
# 2.4.2 @AllArgsConstructor
生成一个包含所有非静态字段的构造器,参数顺序与字段声明顺序一致。
import lombok.AllArgsConstructor;
/**
* 使用 @AllArgsConstructor 生成全参构造器
*/
@AllArgsConstructor
public class Rectangle {
private final int width; // final 字段也会包含
private final int height;
private String color;
}
// 生成的代码:
// public Rectangle(final int width, final int height, String color) {
// this.width = width;
// this.height = height;
// this.color = color;
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.4.3 @RequiredArgsConstructor
生成一个构造器,其参数只包含被 final
修饰且未初始化的字段,以及被 @NonNull
注解标记的字段。
这是实现依赖注入(特别是构造器注入)和创建不可变对象的常用注解。
import lombok.NonNull; // Lombok 提供的非空注解
import lombok.RequiredArgsConstructor;
// 假设存在这些外部依赖接口
interface UserRepository {}
interface AuditService {}
/**
* 使用 @RequiredArgsConstructor 生成必需参数构造器
*/
@RequiredArgsConstructor // 生成包含 repository 和 defaultRole 的构造器
public class UserManager {
// final 字段,必须在构造时初始化
private final UserRepository repository;
// 使用 @NonNull 标记的字段,也必须在构造时初始化,并会自动添加 null 检查
@NonNull
private String defaultRole;
// 非 final 且非 @NonNull 的字段,不会包含在生成的构造器中
private AuditService auditService; // 这个字段需要通过 setter 或其他方式注入/初始化
// 静态字段不参与
private static final int MAX_USERS = 1000;
}
// 生成的代码:
// public UserManager(final UserRepository repository, @NonNull final String defaultRole) {
// // @NonNull 会自动生成 null 检查
// if (defaultRole == null) {
// throw new NullPointerException("defaultRole is marked non-null but is null");
// }
// this.repository = repository;
// this.defaultRole = defaultRole;
// }
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
静态工厂方法: AllArgsConstructor
和 RequiredArgsConstructor
都有一个 staticName
属性,可以用来生成一个静态工厂方法而不是公共构造器,这有助于提高代码可读性或实现特殊创建逻辑。
@RequiredArgsConstructor(staticName = "of") // 生成 public static UserManager of(...) 工厂方法
public class UserManager { /* ... */ }
// 使用: UserManager manager = UserManager.of(repo, "ADMIN");
2
3
# 2.5 @Data
:集大成者
@Data
是一个非常方便的复合注解,它相当于同时应用了以下 5 个注解:
@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
(注意:如果显式定义了任何构造器,则不会生成此构造器)
import lombok.Data;
import lombok.NonNull;
import lombok.ToString;
/**
* 使用 @Data 注解的示例 (通常用于简单的 POJO 或 DTO)
*/
@Data // 包含了 Getter, Setter, ToString, EqualsAndHashCode, RequiredArgsConstructor
public class OrderItem {
private final String productId; // final, 会包含在 RequiredArgsConstructor 中
@NonNull
private Integer quantity; // @NonNull, 会包含在 RequiredArgsConstructor 中, 且有 null 检查
private double unitPrice; // 生成 getter/setter
@ToString.Exclude // 特殊配置:在 ToString 中排除此字段
private String internalNotes; // 生成 getter/setter, 但不在 toString 中
}
// Lombok 会生成:
// - 所有字段的 getter 和 setter (除了 final 的 productId 没有 setter)
// - 基于 productId, quantity, unitPrice 的 equals() 和 hashCode()
// - 排除 internalNotes 的 toString()
// - 一个包含 productId 和 quantity 的 RequiredArgsConstructor
// public OrderItem(final String productId, @NonNull final Integer quantity) { ... }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用 @Data
的注意事项:
- 简单性 vs. 控制力:
@Data
非常适合简单的、纯粹的数据载体类(如 DTO)。但对于包含复杂业务逻辑、需要精确控制字段访问权限或equals
/hashCode
行为的类,建议分开使用@Getter
,@Setter
,@ToString
,@EqualsAndHashCode
等注解,以获得更精细的控制。 @EqualsAndHashCode
的潜在问题:@Data
默认使用所有非静态非 transient 字段生成equals
和hashCode
。在 JPA 实体类中使用@Data
时要特别小心,因为这可能导致在使用 Set 或 Map 时出现问题(例如,对象的哈希码在持久化前后发生变化),或者在比较代理对象和实际对象时出现意外行为。对于 JPA 实体,通常建议使用@Getter
,@Setter
,@ToString
并显式配置@EqualsAndHashCode
(例如@EqualsAndHashCode(onlyExplicitlyIncluded = true)
并标记 ID 字段)。- 构造器: 如果类中显式定义了任何构造器(无论是有参还是无参),
@Data
就不会自动生成@RequiredArgsConstructor
。
# 2.6 @Builder
:优雅地构建对象
@Builder
注解应用建造者 (Builder) 设计模式,为类提供一个流畅的、链式调用的 API 来创建对象实例。这对于具有多个(尤其是可选)字段的类特别有用。
import lombok.Builder;
import lombok.Singular; // 用于处理集合类型的 Builder 方法
import lombok.ToString;
import java.util.List;
import java.util.Map;
/**
* 使用 @Builder 注解的示例
*/
@Builder
@ToString // 方便查看结果
public class HttpClientConfig {
// 必填字段 (可以用 final 或 @NonNull 配合 @RequiredArgsConstructor 或其他方式保证)
private final String baseUrl;
// 可选字段,带有默认值
@Builder.Default // 使用 @Builder.Default 来指定字段的默认值
private int connectTimeoutMillis = 5000; // 默认连接超时 5 秒
@Builder.Default
private int readTimeoutMillis = 10000; // 默认读取超时 10 秒
// 集合类型的处理:使用 @Singular
@Singular // Lombok 会生成 addHeader(key, value) 和 addHeaders(map) 方法
private Map<String, String> headers;
@Singular("allowedMethod") // 可以指定单数形式的方法名 addAllowedMethod(String method)
private List<String> allowedMethods;
// 布尔类型字段
private boolean useProxy;
private String proxyHost;
private int proxyPort;
}
// --- 使用 Builder 创建对象 ---
// HttpClientConfig config1 = HttpClientConfig.builder()
// .baseUrl("https://api.example.com") // 设置必填字段
// // .connectTimeoutMillis(3000) // 可以覆盖默认值
// .useProxy(true) // 设置布尔值
// .proxyHost("proxy.example.com")
// .proxyPort(8080)
// .header("Authorization", "Bearer token123") // 使用 @Singular 生成的单个添加方法
// .header("Accept", "application/json")
// .allowedMethod("GET") // 使用 @Singular 生成的单个添加方法 (指定了名称)
// .allowedMethod("POST")
// // .allowedMethods(Arrays.asList("PUT", "DELETE")) // 也可以一次性设置整个列表
// .build(); // 调用 build() 完成对象创建
// System.out.println(config1);
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
@Builder
的关键特性:
- 内部 Builder 类: Lombok 会生成一个静态的内部 Builder 类 (例如
HttpClientConfigBuilder
)。 - 链式方法: Builder 类为目标类的每个字段生成一个同名的设置方法 (例如
baseUrl(String baseUrl)
),这些方法返回 Builder 自身,允许链式调用。 build()
方法: Builder 类提供一个build()
方法,用于根据设置的值创建目标类的实例。@Builder.Default
: 用于为字段指定默认值。Lombok 会确保即使在 Builder 中没有显式设置该字段,最终的对象也会拥有这个默认值。@Singular
: 用于注解集合或 Map 类型的字段。Lombok 会为该字段生成两个添加方法:一个用于添加单个元素(方法名是字段名的单数形式,例如header(K key, V value)
或allowedMethod(E element)
),另一个用于一次性添加整个集合/Map(方法名是字段名本身,例如headers(Map<? extends K, ? extends V> headers)
)。
高级用法:
toBuilder = true
: 为目标类生成一个toBuilder()
方法,可以基于现有对象创建一个新的 Builder 实例,方便修改对象的某些属性来创建新对象。HttpClientConfig newConfig = oldConfig.toBuilder().connectTimeoutMillis(1000).build();
builderMethodName = "newBuilder"
: 自定义静态方法名,用于获取 Builder 实例 (默认为builder()
)。buildMethodName = "createInstance"
: 自定义构建方法名 (默认为build()
)。builderClassName = "Configurator"
: 自定义 Builder 类的名称 (默认为TargetClassBuilder
)。- 在构造器或静态工厂方法上使用
@Builder
: 可以将@Builder
注解放在构造器或静态工厂方法上,Lombok 会基于该方法的参数来生成 Builder。这在你需要对参数进行校验或转换时很有用。
# 2.7 @Value
:轻松创建不可变对象
@Value
注解是创建不可变 (Immutable) 类的快捷方式。不可变对象一旦创建,其内部状态就不能被修改,这对于并发编程、缓存 key、表示值对象 (Value Object) 等场景非常有用。
@Value
是一个复合注解,大致相当于:
- 将类声明为
final
。 - 将所有字段声明为
private final
。 @Getter
(为所有字段生成 getter)。@AllArgsConstructor
(生成包含所有字段的构造器)。@ToString
。@EqualsAndHashCode
。
注意: @Value
不会生成 setter 方法,因为字段是 final
的。
import lombok.Value;
import lombok.Builder; // 通常与 @Builder 结合使用,方便创建
import lombok.With; // 可选,生成 withXxx 方法用于创建修改了部分字段的新实例
/**
* 使用 @Value 创建不可变类的示例
*/
@Value // 标记此类为不可变值对象
@Builder // 提供 Builder 模式创建对象
public class Coordinate {
// 所有字段自动变为 private final
int x;
int y;
String label;
// 使用 @With 注解可以为字段生成一个 "with" 方法
// 调用 withXxx(newValue) 会返回一个新的 Coordinate 实例,
// 其中只有 xxx 字段被更新,其他字段保持不变。
@With(AccessLevel.PUBLIC) // 可以指定 with 方法的访问级别
String description;
}
// --- 创建和使用 ---
// Coordinate coord1 = Coordinate.builder()
// .x(10)
// .y(20)
// .label("Point A")
// .description("Initial point")
// .build();
// System.out.println(coord1); // 输出: Coordinate(x=10, y=20, label=Point A, description=Initial point)
// 尝试修改字段 (会编译失败,因为没有 setter 且字段是 final)
// coord1.setX(15); // Error: cannot find symbol method setX(int)
// 使用 @With 生成的方法创建新实例
// Coordinate coord2 = coord1.withDescription("Updated description");
// System.out.println(coord2); // 输出: Coordinate(x=10, y=20, label=Point A, description=Updated description)
// System.out.println(coord1 == coord2); // 输出: false (是新对象)
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
@Value
生成的代码(示意):
public final class Coordinate { ... }
private final int x;
private final int y;
private final String label;
private final String description;
public int getX() { return this.x; }
(所有字段的 getter)public Coordinate(int x, int y, String label, String description) { ... }
(全参构造器)public String toString() { ... }
public boolean equals(Object o) { ... }
public int hashCode() { ... }
public Coordinate withDescription(String description) { return new Coordinate(this.x, this.y, this.label, description); }
(由@With
生成)
最佳实践:
- 非常适合用于表示值对象 (Value Objects),例如坐标、金额、配置参数等,这些对象的值一旦确定就不应改变。
- 保证了线程安全,因为状态不可变。
- 通常与
@Builder
配合使用,使创建过程更方便。 @With
注解提供了在保持不可变性的前提下,“修改”对象(实际是创建新对象)的便捷方式。
# 2.8 日志相关注解:@Log
系列
Lombok 提供了一系列注解,可以快速地在类中自动创建一个静态的、final
的日志记录器 (Logger) 实例,字段名通常是 log
。
# 2.8.1 @Slf4j
(推荐)
这是最常用的日志注解,用于生成一个 SLF4j (Simple Logging Facade for Java) 的 Logger 实例。你需要确保项目中已经正确配置了 SLF4j API 和一个 SLF4j 绑定(如 Logback 或 Log4j 2)。
import lombok.extern.slf4j.Slf4j; // 引入 Slf4j 注解
/**
* 使用 @Slf4j 注解自动生成 SLF4j Logger
*/
@Slf4j // Lombok 会在编译时添加一个名为 log 的 SLF4j Logger 字段
public class DataProcessor {
public void processData(String dataId) {
// 可以直接使用 log 变量进行日志记录
log.info("开始处理数据,ID: {}", dataId); // 使用 SLF4j 的占位符语法
if (dataId == null) {
log.warn("接收到的数据 ID 为 null!");
return;
}
try {
// 模拟处理过程
log.debug("正在执行复杂计算,数据 ID: {}", dataId);
Thread.sleep(50); // 模拟耗时
if (dataId.contains("error")) {
throw new IllegalArgumentException("模拟处理错误");
}
log.info("数据处理成功,ID: {}", dataId);
} catch (Exception e) {
// 记录错误及堆栈信息
log.error("处理数据时发生错误,ID: {}", dataId, e);
// 可以选择重新抛出或处理异常
}
}
}
// 生成的代码(示意):
// import org.slf4j.Logger;
// import org.slf4j.LoggerFactory;
//
// public class DataProcessor {
// // Lombok 自动生成的静态 final Logger 字段
// private static final Logger log = LoggerFactory.getLogger(DataProcessor.class);
//
// // ... 原有方法 ...
// }
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
# 2.8.2 其他日志框架注解
Lombok 也支持直接生成其他常见日志框架的 Logger 实例:
@Log
: 生成java.util.logging.Logger
实例。@Log4j
: 生成 Log4j 1.x 的org.apache.log4j.Logger
实例。@Log4j2
: 生成 Log4j 2 的org.apache.logging.log4j.Logger
实例。@CommonsLog
: 生成 Apache Commons Logging 的org.apache.commons.logging.Log
实例。@JBossLog
: 生成 JBoss Logging 的org.jboss.logging.Logger
实例。
import lombok.extern.java.Log; // java.util.logging
import lombok.extern.log4j.Log4j; // Log4j 1.x
import lombok.extern.log4j.Log4j2; // Log4j 2.x
import lombok.extern.apachecommons.CommonsLog; // Apache Commons Logging
/**
* 演示使用其他日志框架注解
*/
@Log // 使用 java.util.logging
class JulLoggingService {
public void serve() {
// 生成的 log 字段类型是 java.util.logging.Logger
log.info("Serving using JUL...");
log.warning("This is a JUL warning.");
}
}
@Log4j2 // 使用 Log4j 2
class Log4j2Service {
public void serve() {
// 生成的 log 字段类型是 org.apache.logging.log4j.Logger
log.info("Serving using Log4j2...");
log.error("This is a Log4j2 error.");
}
}
@CommonsLog // 使用 Apache Commons Logging
class CommonsLoggingService {
public void serve() {
// 生成的 log 字段类型是 org.apache.commons.logging.Log
log.info("Serving using Commons Logging...");
log.fatal("This is a Commons Logging fatal message.");
}
}
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
选择哪个?: 强烈推荐使用 @Slf4j
,因为它遵循了面向接口编程的原则,让你的代码与具体的日志实现解耦。只有在特定项目或遗留系统强制要求使用其他日志框架 API 时,才考虑使用对应的 @Log
系列注解。
# 2.9 @NonNull
:编译期非空检查
@NonNull
注解可以用于字段、方法参数、方法返回值(实验性)、局部变量。Lombok 会在相应的位置自动生成空指针检查 (Null Check) 代码。
- 用于字段: 如果与
@RequiredArgsConstructor
或@Builder
一起使用,会在构造器或 Builder 的build()
方法中添加非空检查。 - 用于方法参数: 会在方法体的开头为该参数插入非空检查代码。如果参数为
null
,会抛出NullPointerException
。
import lombok.NonNull;
import java.time.LocalDateTime;
/**
* 使用 @NonNull 进行非空检查
*/
public class NotificationService {
// 用于方法参数:Lombok 会在方法入口处添加 null 检查
public void sendNotification(@NonNull String recipientId,
@NonNull String message,
String senderId /* 可为 null */) {
// 如果 recipientId 或 message 为 null,在执行到这里之前就会抛出 NPE
System.out.println("Sending notification to " + recipientId);
System.out.println("Message: " + message);
if (senderId != null) {
System.out.println("From: " + senderId);
}
// ... 发送逻辑 ...
}
// 配合 @RequiredArgsConstructor (或 @Builder)
@RequiredArgsConstructor
static class UserPreferences {
@NonNull // 标记此字段在构造时必须非空
private final String userId;
private String theme = "default"; // 可选字段
}
// 也可以用于局部变量 (虽然不太常用)
public void process() {
@NonNull String importantValue = getImportantValue();
// 如果 getImportantValue() 返回 null,这里会抛出 NPE
System.out.println("Processing value: " + importantValue.toLowerCase());
}
private String getImportantValue() {
// 模拟可能返回 null 的方法
return Math.random() > 0.1 ? "SomeValue" : null;
}
}
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
Lombok 生成的代码(示意) - sendNotification
方法:
public void sendNotification(String recipientId, String message, String senderId) {
// Lombok 自动生成的非空检查
if (recipientId == null) {
throw new NullPointerException("recipientId is marked non-null but is null");
}
if (message == null) {
throw new NullPointerException("message is marked non-null but is null");
}
// --- 原有方法体 ---
System.out.println("Sending notification to " + recipientId);
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
优点:
- Fail-Fast: 让空指针异常尽早发生,更容易定位问题。
- 代码简洁: 避免了在方法体中手动编写大量的
if (param == null)
检查。 - 明确契约:
@NonNull
明确地表达了方法或构造器对其参数的非空要求。
# 2.10 @Cleanup
:自动资源管理
@Cleanup
注解用于保证被注解的局部变量(必须是实现了 close()
方法或其他指定清理方法的资源,如 InputStream
, OutputStream
, Reader
, Writer
, Connection
等)在当前作用域结束时自动调用其清理方法。这类似于 Java 7 的 try-with-resources
语句,但 @Cleanup
可以在 try-with-resources
不适用的地方(例如旧版 Java 或更复杂的控制流)提供便利。
import lombok.Cleanup; // 引入 Cleanup 注解
import java.io.*;
/**
* 使用 @Cleanup 自动关闭资源
*/
public class FileCopier {
public void copyFile(String sourcePath, String destPath) throws IOException {
System.out.println("开始复制文件...");
// 使用 @Cleanup 注解标记需要自动关闭的资源
// Lombok 会在包含此变量的作用域结束时(正常退出或异常退出)
// 自动生成调用 inputStream.close() 的 finally 代码块
@Cleanup InputStream inputStream = new FileInputStream(sourcePath);
@Cleanup OutputStream outputStream = new FileOutputStream(destPath);
byte[] buffer = new byte[4096]; // 缓冲区
int bytesRead;
// 执行文件复制逻辑
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成!");
// 方法结束时,inputStream 和 outputStream 会被自动关闭,无需手动编写 finally { close(); }
}
// 演示自定义清理方法名称
public void processWithCustomCleanup() {
// 假设 MyResource 有一个 dispose() 方法用于清理
@Cleanup("dispose") // 指定调用 dispose() 而不是 close()
MyResource resource = new MyResource();
resource.use();
// 方法结束时,会自动调用 resource.dispose()
}
// 模拟需要清理的资源类
static class MyResource {
public void use() { System.out.println("Using MyResource..."); }
public void dispose() { System.out.println("Disposing MyResource..."); }
// 注意:如果使用默认 @Cleanup,此类需要实现 AutoCloseable 或 Closeable
// 或者有一个无参的 public void close() 方法
}
}
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
Lombok 生成的代码(示意) - copyFile
方法:
public void copyFile(String sourcePath, String destPath) throws IOException {
System.out.println("开始复制文件...");
InputStream inputStream = new FileInputStream(sourcePath);
try { // 外层 try-finally 对应第一个 @Cleanup
OutputStream outputStream = new FileOutputStream(destPath);
try { // 内层 try-finally 对应第二个 @Cleanup
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成!");
} finally {
// 自动生成的 outputStream 关闭逻辑
if (outputStream != null) {
outputStream.close();
}
}
} finally {
// 自动生成的 inputStream 关闭逻辑
if (inputStream != null) {
inputStream.close();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用说明:
@Cleanup
只能用于局部变量。- 默认情况下,Lombok 会查找并调用名为
close()
的无参方法。 - 可以通过
@Cleanup("methodName")
来指定需要调用的清理方法的名称。 - 被注解的变量类型需要有关闭/清理方法,否则编译时会报错。
- 虽然方便,但在 Java 7 及以上版本中,对于标准的
AutoCloseable
资源,推荐优先使用try-with-resources
语句,因为它更标准、更清晰。@Cleanup
在某些try-with-resources
不易应用的场景下(如需要对关闭异常进行特殊处理,或在不支持try-with-resources
的旧代码中)仍然有用。
# 三、Lombok 常见问题与解决方案
虽然 Lombok 非常方便,但在使用过程中也可能遇到一些问题。
问题类型 | 具体问题描述 | 可能原因与解决方案 |
---|---|---|
IDE 集成 | IDEA/Eclipse 中代码标红,提示找不到 getter/setter 等方法 | 1. 未安装/启用 Lombok 插件: 确保对应 IDE 的 Lombok 插件已正确安装并启用。对于 IDEA,检查 Annotation Processing 是否开启。 2. 插件版本不兼容: 尝试更新 Lombok 插件到最新版。 3. IDE 缓存问题: 重启 IDE,或执行 File -> Invalidate Caches / Restart (IDEA) / Project -> Clean (Eclipse)。 4. 配置错误: 检查项目依赖中的 Lombok 版本是否正确,Gradle/Maven 配置是否完整(特别是 annotationProcessor )。 |
编译问题 | Maven/Gradle 编译时报错 "cannot find symbol" (找不到生成的符号) | 1. 依赖配置不全: 确保 Maven 的 <scope>provided</scope> 或 Gradle 的 compileOnly 和 annotationProcessor 都已正确配置。 2. 版本冲突: 检查项目中是否有其他库间接引入了不同版本的 Lombok,尝试统一版本。 3. 编译器问题: 确保使用的 JDK 版本与 Lombok 兼容。尝试更新 JDK 或 Lombok 版本。 |
框架兼容 | 在 JPA/Hibernate 实体类中使用 @Data 或 @EqualsAndHashCode 导致问题 (如 Set 中对象重复、懒加载异常等) | 1. @EqualsAndHashCode 问题: 不要在 JPA 实体类上直接使用默认的 @EqualsAndHashCode (或 @Data )。因为实体对象的哈希码可能基于可变字段或 ID (ID 在持久化前后可能变化)。 推荐: 使用 @EqualsAndHashCode(onlyExplicitlyIncluded = true) 并只包含稳定的业务主键 (@EqualsAndHashCode.Include 标记在业务键字段上),或者只使用数据库生成的 ID (@EqualsAndHashCode(of = "id") ) 但要小心其在持久化前的比较行为。 2. @ToString 问题: 默认的 @ToString 可能触发懒加载关系,导致 LazyInitializationException 。在关系字段上使用 @ToString.Exclude 。 3. 构造器问题: JPA 要求实体类必须有一个 public 或 protected 的无参构造器。确保使用 @NoArgsConstructor (如果需要) 且访问级别正确。如果使用了 @AllArgsConstructor 或 @RequiredArgsConstructor ,可能需要手动添加 @NoArgsConstructor 。 |
框架兼容 | Jackson/Gson 等序列化/反序列化库出现问题 | 1. 无参构造器: 确保类有 Jackson 等库需要的无参构造器 (@NoArgsConstructor )。 2. 字段可见性: Lombok 生成的 getter/setter 默认是 public,通常兼容。如果遇到问题,检查是否有自定义的访问级别或命名策略。 3. @Builder 与反序列化: @Builder 生成的类通常没有默认的 setter,可能与反序列化不兼容。可以考虑添加 @NoArgsConstructor 和 @AllArgsConstructor (如果需要反序列化器使用构造器),或者为 DTO 单独创建类。 |
构造器冲突 | 同时使用 @Builder 和 @XArgsConstructor (如 @AllArgsConstructor ) | 1. Lombok 行为: @Builder 默认会尝试生成一个全参构造器(或基于 @Builder 所在方法的参数)。如果同时存在 @AllArgsConstructor ,可能会冲突或行为不确定。 2. 解决方案: 通常建议不要同时在类级别使用 @Builder 和 @AllArgsConstructor 。如果需要全参构造器,可以显式定义,或者将 @Builder 注解放在静态工厂方法或构造器上,而不是类上。如果确实需要同时使用,可以通过调整注解顺序或查阅 Lombok 文档了解特定版本下的精确行为。 |
代码混淆 | 使用 ProGuard 等工具混淆代码后,Lombok 生成的方法丢失或出错 | 确保混淆配置中保留 (keep) Lombok 生成的方法(如 getter/setter/canEqual 等)以及注解本身。查阅 Lombok 和混淆工具的文档获取具体的配置规则。 |
调试困难 | 无法单步调试进入 Lombok 生成的代码内部 | Lombok 生成的代码是在编译期注入的,源代码中并不存在。通常无法直接单步调试。可以通过反编译 .class 文件查看生成的代码,或者在调用生成方法的位置打断点来观察输入输出。 |
日志变量 | 使用 @Slf4j 等注解后,IDE 中无法直接跳转到 log 字段的定义 | 这是正常现象,因为 log 字段是在编译期生成的。只要编译通过且运行时日志正常输出,就无需担心。IDE 插件通常能识别 log 变量并提供日志方法的代码提示。 |
# 四、Lombok 进阶技巧与最佳实践
掌握 Lombok 的基本注解后,可以通过一些进阶技巧和最佳实践进一步提升代码质量和开发效率。
# 4.1 组合使用注解
根据类的不同职责,灵活组合使用 Lombok 注解可以达到最佳效果。
import lombok.*;
import java.time.LocalDateTime;
/**
* 演示注解的灵活组合
*/
@Getter // 为所有字段生成 getter
// @Setter // 可能不需要所有字段都有 setter,或者需要不同访问级别,所以不在类级别统一添加
@ToString(exclude = {"passwordHash", "internalData"}) // 定制 toString
@EqualsAndHashCode(of = {"username"}) // 仅基于 username 判断相等性
// 可能需要无参构造器用于框架(如 JPA, Jackson)
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 提供 protected 无参构造器
// 可能需要一个用于创建对象的全参构造器(或使用 @Builder)
@AllArgsConstructor(access = AccessLevel.PUBLIC)
public class AdvancedUser {
private Long id;
@Setter(AccessLevel.PROTECTED) // username 允许 protected 级别的修改
private String username;
// 密码哈希通常不允许外部直接设置
@Getter(AccessLevel.NONE) // 不生成 getPasswordHash()
private String passwordHash;
// 邮箱可以公开设置
@Setter(AccessLevel.PUBLIC)
private String email;
private final LocalDateTime registrationTime = LocalDateTime.now(); // final 字段,无 setter
private transient Object internalData; // transient 字段,通常排除
// 手动提供一个设置密码的方法,包含加密逻辑
public void changePassword(String rawPassword) {
if (rawPassword == null || rawPassword.length() < 8) {
throw new IllegalArgumentException("Password too short");
}
// this.passwordHash = encryptPassword(rawPassword); // 假设有加密方法
}
}
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
要点:
- 不要滥用
@Data
,根据需要精确选择@Getter
,@Setter
,@ToString
,@EqualsAndHashCode
,@XArgsConstructor
。 - 利用
AccessLevel
控制生成方法的访问权限。 - 利用注解的
exclude
,of
,onlyExplicitlyIncluded
等属性精细控制行为。 - 结合
final
和@NonNull
来定义必需字段和不变性。
# 4.2 不同类型实体的 Lombok 最佳实践
# 4.2.1 数据传输对象 (DTO / VO)
DTO 通常用于在不同层(如 Service 层和 Controller 层)或系统间传递数据,一般是简单的、可变的 Pojo。
import lombok.Data; // @Data 很适合简单的 DTO
import lombok.Builder; // @Builder 方便测试或手动创建 DTO
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List;
/**
* 用户信息的 DTO 示例
*/
@Data // 提供 getter, setter, toString, equals, hashCode, RequiredArgsConstructor
@NoArgsConstructor // 提供无参构造器 (Jackson 等可能需要)
@AllArgsConstructor // 提供全参构造器 (方便创建)
@Builder // 提供 Builder 模式 (方便测试或链式创建)
public class UserView {
private Long userId;
private String displayName;
private String emailAddress;
private List<String> permissions;
private boolean isActive;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 4.2.2 持久化实体 (JPA Entity)
JPA 实体类与数据库表映射,使用 Lombok 时要特别注意 @EqualsAndHashCode
和 @ToString
的潜在问题。
import lombok.*;
import javax.persistence.*; // JPA 注解
import java.time.LocalDateTime;
import java.util.Set;
/**
* JPA 实体类的 Lombok 使用建议
*/
@Entity // 标记为 JPA 实体
@Table(name = "app_users") // 指定表名
@Getter // 通常需要 getter
@Setter // 通常需要 setter
// @ToString 要小心,排除关联字段避免懒加载问题和循环引用
@ToString(exclude = {"roles", "orders"})
// @EqualsAndHashCode 强烈建议只基于 ID 或稳定业务键,且显式指定
@EqualsAndHashCode(of = "id") // 仅基于 id 比较;注意 id 在持久化前可能为 null
// 或者 @EqualsAndHashCode(onlyExplicitlyIncluded = true) + @Include 在业务键上
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 需要无参构造器 (可以是 protected)
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 全参构造器通常设为 private 或 protected,通过 Builder 或工厂方法创建
@Builder // 可以提供 Builder
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
// 密码通常不在实体中直接存储明文,且不应包含在 toString/equals/hashCode 中
@Column(nullable = false)
@ToString.Exclude // 排除
private String passwordHash;
@Column(unique = true, length = 100)
private String email;
@Column(nullable = false)
private boolean enabled = true;
@Column(updatable = false) // 创建后不可更新
private LocalDateTime createdAt;
// 关联关系,注意懒加载和 ToString/EqualsAndHashCode 的影响
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "app_user_roles", /* ... join columns ... */)
@ToString.Exclude // 必须排除,否则可能触发懒加载
private Set<Role> roles;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@ToString.Exclude
private Set<Order> orders;
@PrePersist // JPA 生命周期回调,用于设置创建时间
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
}
// 假设 Role 和 Order 也是实体类...
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
# 4.2.3 不可变值对象 (Value Object)
值对象(如金额、坐标、配置项)的核心是其值,通常是不可变的。
import lombok.Value; // 核心注解,创建不可变类
import lombok.Builder; // 方便创建
import lombok.With; // 提供非破坏性修改方法
import java.math.BigDecimal;
/**
* 不可变金额值对象示例
*/
@Value // 标记为不可变,自动 final 类、final 字段、getter、全参构造、toString、equals/hashCode
@Builder // 提供 Builder
public class Money {
@NonNull // 币种不能为空
String currency;
@NonNull // 金额不能为空
BigDecimal amount;
// 使用 @With 生成 withAmount 方法,返回带有新金额的新 Money 对象
@With(AccessLevel.PUBLIC)
BigDecimal discountAmount; // 可选的折扣金额
// 可以添加业务方法
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return Money.builder()
.currency(this.currency)
.amount(this.amount.add(other.amount))
.discountAmount(this.discountAmount) // 继承可选字段
.build();
}
}
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
# 4.3 Lombok 与设计模式的结合
# 4.3.1 建造者模式 (Builder Pattern) - 高级应用
除了基本的对象构建,@Builder
还可以用在方法上,创建特定场景的静态工厂方法,或者通过 @Builder.Default
和 @Singular
处理复杂情况。
import lombok.*;
import java.util.List;
import java.util.Map;
/**
* 高级 Builder 用法示例
*/
@Getter // 为了方便访问字段
@ToString
public class ReportRequest {
private final String reportName;
private final String format; // PDF, CSV, etc.
private final List<String> recipients;
private final Map<String, Object> parameters;
private final boolean runImmediately;
private final int priority;
// 使用 @Builder 注解在构造器上,可以在构造器中添加校验逻辑
@Builder // Lombok 会基于此构造器的参数生成 Builder
private ReportRequest(@NonNull String reportName,
String format, // 可选参数
@Singular List<String> recipients, // 使用 @Singular
@Singular Map<String, Object> parameters,
boolean runImmediately,
@Builder.Default int priority = 5) { // 默认值在参数上指定
// 在构造器中添加校验逻辑
if (reportName.isEmpty()) {
throw new IllegalArgumentException("Report name cannot be empty");
}
this.reportName = reportName;
this.format = (format == null || format.isEmpty()) ? "PDF" : format; // 默认格式
this.recipients = recipients;
this.parameters = parameters;
this.runImmediately = runImmediately;
this.priority = priority;
}
// 可以在方法上使用 @Builder 创建特定类型的快捷方式
@Builder(builderMethodName = "urgentCsvReportBuilder", buildMethodName="buildUrgentCsv")
public static ReportRequest createUrgentCsvReport(@NonNull String reportName,
@Singular List<String> recipients) {
return ReportRequest.builder() // 调用上面构造器生成的 builder
.reportName(reportName)
.format("CSV")
.recipients(recipients)
.runImmediately(true)
.priority(1) // 高优先级
.build();
}
}
// --- 使用示例 ---
// ReportRequest standardReport = ReportRequest.builder()
// .reportName("Monthly Sales")
// .recipient("manager@example.com")
// .parameter("month", "2024-03")
// .build(); // 使用构造器上的 Builder
// ReportRequest urgentReport = ReportRequest.urgentCsvReportBuilder() // 使用方法上的 Builder
// .reportName("Urgent Stock Alert")
// .recipient("ops@example.com")
// .recipient("ceo@example.com")
// .buildUrgentCsv(); // 使用自定义的 build 方法名
// System.out.println(standardReport);
// System.out.println(urgentReport);
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
# 4.3.2 工厂方法模式 (Factory Method Pattern)
虽然 Lombok 不直接生成工厂方法模式,但 @Value
, @Builder
, @RequiredArgsConstructor(staticName=...)
等可以辅助实现该模式,特别是创建不可变对象或具有复杂创建逻辑的对象时。
import lombok.*;
/**
* 结合 Lombok 实现工厂方法的示例
*/
interface Notification { void send(); }
@Value // 使用 @Value 创建不可变的具体通知类
class EmailNotification implements Notification {
String recipient;
String subject;
String body;
@Override public void send() { System.out.println("Sending Email to " + recipient); }
}
@Value
class SmsNotification implements Notification {
String phoneNumber;
String message;
@Override public void send() { System.out.println("Sending SMS to " + phoneNumber); }
}
// 工厂类
public class NotificationFactory {
// 使用 Lombok 辅助创建参数对象 (如果参数复杂)
@Builder @Value public static class EmailParams { String recipient; String subject; String body; }
@Builder @Value public static class SmsParams { String phoneNumber; String message; }
// 工厂方法,根据类型创建不同的通知实例
public static Notification createNotification(Object params) {
if (params instanceof EmailParams) {
EmailParams p = (EmailParams) params;
// @Value 生成了全参构造器
return new EmailNotification(p.getRecipient(), p.getSubject(), p.getBody());
} else if (params instanceof SmsParams) {
SmsParams p = (SmsParams) params;
return new SmsNotification(p.getPhoneNumber(), p.getMessage());
}
throw new IllegalArgumentException("Unsupported notification parameters type");
}
// 提供更便捷的静态工厂方法
public static Notification createEmail(String recipient, String subject, String body) {
return new EmailNotification(recipient, subject, body);
}
public static Notification createSms(String phoneNumber, String message) {
return new SmsNotification(phoneNumber, message);
}
}
// --- 使用工厂 ---
// Notification email = NotificationFactory.createEmail("test@example.com", "Hello", "World");
// Notification sms = NotificationFactory.createSms("1234567890", "Meeting reminder");
// email.send();
// sms.send();
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
# 4.4 与其他框架集成的最佳实践
# 4.4.1 Spring 框架集成
- 构造器注入: 使用
@RequiredArgsConstructor
配合final
字段是实现构造器注入(Spring 推荐的方式)的最简洁方式,可以完全替代@Autowired
注解在构造器或字段上。 - 日志: 使用
@Slf4j
(或其他@Log
系列) 简化 Controller, Service, Component 中的日志对象创建。 - 配置类: 使用
@Data
或@ConfigurationProperties
配合@Getter/@Setter
(或@Value
如果配置是不可变的) 来绑定配置文件。
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value; // Spring 的 Value 注解
// 假设存在 ProductRepository 接口
interface ProductRepository { /* ... */ }
class Product { /* ... */ }
class ProductNotFoundException extends RuntimeException { ProductNotFoundException(String msg){ super(msg); }}
/**
* Spring Service 类与 Lombok 结合的推荐实践
*/
@Service // 标记为 Spring Bean
@RequiredArgsConstructor // 生成包含 final 字段的构造器,用于依赖注入
@Slf4j // 自动创建 log 字段
public class ProductCatalogService {
// 使用 final 标记需要注入的依赖,@RequiredArgsConstructor 会处理它们
private final ProductRepository productRepository;
private final PricingService pricingService; // 假设有另一个服务
@Value("${catalog.service.default-currency:USD}") // 注入配置属性
private String defaultCurrency; // 非 final 字段,通过 Spring 的 @Value 注入
public Product getProductDetails(String productId) {
log.info("Fetching details for product ID: {}", productId);
Product product = productRepository.findById(productId) // 假设方法存在
.orElseThrow(() -> {
log.warn("Product not found for ID: {}", productId);
return new ProductNotFoundException("Product not found: " + productId);
});
// 调用其他服务
double price = pricingService.calculatePrice(product, defaultCurrency);
// product.setPrice(price); // 假设 Product 有 setPrice
log.debug("Product details retrieved for ID: {}", productId);
return product;
}
}
// 假设 PricingService
@Service
class PricingService {
public double calculatePrice(Product p, String currency) { /* ... */ return 10.0; }
}
// 假设 ProductRepository
interface ProductRepository { java.util.Optional<Product> findById(String id); }
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
# 4.4.2 JPA / Hibernate 集成
如前所述,在 JPA 实体中使用 Lombok 需要特别注意 @EqualsAndHashCode
和 @ToString
,并确保存在符合 JPA 要求的构造器。
总结建议:
- 使用
@Getter
,@Setter
。 - 使用
@ToString(exclude = {"关联字段1", "关联字段2"})
排除懒加载和循环引用的字段。 - 使用
@EqualsAndHashCode(of = "id")
或@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@Include
在稳定业务键上。 - 确保有
@NoArgsConstructor(access = AccessLevel.PROTECTED)
。 - 可以添加
@Builder
或@AllArgsConstructor(access = AccessLevel.PRIVATE/PROTECTED)
用于方便创建实例(例如测试或数据初始化)。
// (重复上面 4.2.2 的 JPA 实体示例,强调注解选择)
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Table(name = "items")
@Getter @Setter // 提供 Getter 和 Setter
@ToString(exclude = "order") // 排除关联字段
@EqualsAndHashCode(of = "id") // 基于 ID 判断相等性
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 需要
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false) private String productId;
@Column(nullable = false) private int quantity;
@Column(nullable = false) private java.math.BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY) // 延迟加载
@JoinColumn(name = "order_id", nullable = false)
@ToString.Exclude // 必须排除
private Order order; // 关联到 Order 实体
// 可以添加 Builder 用于测试或数据准备
@Builder
public OrderItem(String productId, int quantity, java.math.BigDecimal price, Order order) {
this.productId = productId;
this.quantity = quantity;
this.price = price;
this.order = order;
}
}
// 假设 Order 类也按类似原则配置了 Lombok 注解
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
结语
Lombok 是一个极其强大的工具,能够显著减少 Java 开发中的样板代码,让代码更简洁、更易读、更易维护。通过熟练掌握其核心注解和最佳实践,结合具体的应用场景(如 DTO、实体、值对象)和框架(如 Spring、JPA),可以大幅提升开发效率和代码质量。然而,也要注意 Lombok 的工作原理(编译期代码生成)及其可能带来的问题(IDE 依赖、框架兼容性、调试限制),并遵循最佳实践来规避这些问题。合理地使用 Lombok,它将成为你 Java 开发工具箱中的一把利器。