程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java常用开发工具包

    • Jackson(JSON处理库)
    • FastJson2(JSON处理库)
    • Gson(JSON处理库)
    • BeanUtils(对象复制工具)
    • MapStruct(对象转换工具)
    • Guava(开发工具包)
    • ThreadLocal(本地线程变量)
    • SLF4j(日志框架)
    • Lombok (注解插件)
      • 一、Lombok 的安装与配置
      • 二、Lombok 核心注解详解
        • 2.1 @Getter 与 @Setter:告别手动 getter/setter
        • 2.2 @ToString:智能生成 toString()
        • 2.3 @EqualsAndHashCode:安全生成 equals() 和 hashCode()
        • 2.4 构造器相关注解
        • 2.4.1 @NoArgsConstructor
        • 2.4.2 @AllArgsConstructor
        • 2.4.3 @RequiredArgsConstructor
        • 2.5 @Data:集大成者
        • 2.6 @Builder:优雅地构建对象
        • 2.7 @Value:轻松创建不可变对象
        • 2.8 日志相关注解:@Log 系列
        • 2.8.1 @Slf4j (推荐)
        • 2.8.2 其他日志框架注解
        • 2.9 @NonNull:编译期非空检查
        • 2.10 @Cleanup:自动资源管理
      • 三、Lombok 常见问题与解决方案
      • 四、Lombok 进阶技巧与最佳实践
        • 4.1 组合使用注解
        • 4.2 不同类型实体的 Lombok 最佳实践
        • 4.2.1 数据传输对象 (DTO / VO)
        • 4.2.2 持久化实体 (JPA Entity)
        • 4.2.3 不可变值对象 (Value Object)
        • 4.3 Lombok 与设计模式的结合
        • 4.3.1 建造者模式 (Builder Pattern) - 高级应用
        • 4.3.2 工厂方法模式 (Factory Method Pattern)
        • 4.4 与其他框架集成的最佳实践
        • 4.4.1 Spring 框架集成
        • 4.4.2 JPA / Hibernate 集成
  • 开发工具包
  • Java常用开发工具包
scholar
2025-03-29
目录

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>
1
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")
// }
1
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 配置:

    1. 安装 Lombok 插件: 打开 File → Settings (或 Preferences on macOS) → Plugins。在 Marketplace 中搜索 "Lombok" 并安装。安装后可能需要重启 IDEA。
    2. 启用注解处理: 打开 File → Settings (或 Preferences) → Build, Execution, Deployment → Compiler → Annotation Processors。确保勾选了 "Enable annotation processing"。对于较新版本的 IDEA,这一步通常在安装插件后会自动完成或不再需要手动配置。
  • Eclipse 配置:

    1. 下载 lombok.jar: 从 Lombok 官网 (opens new window) 或 Maven 仓库下载对应版本的 lombok.jar 文件。
    2. 运行安装程序: 关闭 Eclipse。在命令行中运行 java -jar lombok.jar。这会启动一个图形化安装界面。
    3. 指定 Eclipse 安装位置: 在安装界面中,它通常会自动检测到你的 Eclipse 安装。如果没有,手动指定 Eclipse 的安装目录。
    4. 点击 "Install / Update": 完成安装。
    5. 重启 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;
    }
}
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

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;
    }

    // ... 原有方法和构造函数 ...
}
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

常用配置参数:

  • 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; }
}
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

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 + // 自定义名称和顺序
           ")";
}
1
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) { ... }
}
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

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;
}
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

常用配置参数:

  • 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() {}
1
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;
// }
1
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;
// }
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

静态工厂方法: AllArgsConstructor 和 RequiredArgsConstructor 都有一个 staticName 属性,可以用来生成一个静态工厂方法而不是公共构造器,这有助于提高代码可读性或实现特殊创建逻辑。

@RequiredArgsConstructor(staticName = "of") // 生成 public static UserManager of(...) 工厂方法
public class UserManager { /* ... */ }
// 使用: UserManager manager = UserManager.of(repo, "ADMIN");
1
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) { ... }
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

使用 @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);
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

@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 (是新对象)
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

@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);
//
//     // ... 原有方法 ...
// }
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

# 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.");
    }
}
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

选择哪个?: 强烈推荐使用 @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;
    }
}
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

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);
    // ...
}
1
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() 方法
    }
}
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

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();
        }
    }
}
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

使用说明:

  • @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); // 假设有加密方法
    }
}
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

要点:

  • 不要滥用 @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;
}
1
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 也是实体类...
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

# 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();
    }
}
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

# 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);
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

# 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();
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

# 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); }
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

# 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 注解
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

结语

Lombok 是一个极其强大的工具,能够显著减少 Java 开发中的样板代码,让代码更简洁、更易读、更易维护。通过熟练掌握其核心注解和最佳实践,结合具体的应用场景(如 DTO、实体、值对象)和框架(如 Spring、JPA),可以大幅提升开发效率和代码质量。然而,也要注意 Lombok 的工作原理(编译期代码生成)及其可能带来的问题(IDE 依赖、框架兼容性、调试限制),并遵循最佳实践来规避这些问题。合理地使用 Lombok,它将成为你 Java 开发工具箱中的一把利器。

编辑此页 (opens new window)
上次更新: 2025/04/06, 10:15:45
SLF4j(日志框架)

← SLF4j(日志框架)

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式