1. 概述
Java 是一种很棒的语言,但有时对于我们在代码中或符合某些框架实践需要执行的常见任务而言,它可能会变得过于冗长。这通常不会为我们程序的业务方带来真正的价值,而 Lombok 就应运而生,使我们更具生产力。
它通过插入到我们的构建过程中,并根据我们在代码中引入的一些项目注释自动生成 Java 字节码到我们的 .class 文件中来实现。
我们可以将 Project Lombok Java 库插入到构建工具中,以自动执行一些代码,例如 getter/setter 方法和日志变量。
在本教程中,我们将讨论如何使用 Lombok 与 Maven 结合使用以利用其中的一些功能。
2. Maven 项目设置
无论我们使用哪种系统,将 Project Lombok 包含在我们的构建中都非常简单明了。Project Lombok 的 项目页面 提供了关于具体操作的详细说明。我们可以将 Maven Lombok 依赖项以 provided 范围的形式放入
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
依赖 Lombok 不会使我们的 .jar 的使用者也依赖它,因为它是一个纯构建依赖项,而不是运行时依赖项。
3. 局部变量的类型推断
从 Lombok 1.16.20 开始,Lombok 支持使用 var 从初始化表达式推断局部变量的类型。Lombok 1.18.22 支持使用 val 进行最终局部变量的类型推断。换句话说,Lombok 将 val 替换为 final var。 但是,我们不能在字段中使用此功能。
让我们通过一个例子演示 Lombok 局部变量的类型推断
public String lombokTypeInferred() {
val list = new ArrayList<String>();
list.add("Hello, Lombok!");
val listElem = list.get(0);
return listElem.toLowerCase();
}
自动代码生成使 list 成为类型为 ArrayList<String> 的 final 变量。
4. Getter/Setter、构造函数
通过公共 getter 和 setter 方法封装对象属性是 Java 中的常见做法,许多框架广泛依赖于这种“Java Bean”模式(一个具有空构造函数和“属性”的 getter/setter 方法的类)。
这非常普遍,大多数 IDE 支持自动生成这些模式(以及更多)的代码。但是,这段代码需要存在于我们的源代码中,并在添加新属性或重命名字段时进行维护。
让我们考虑一下我们想要用作 JPA 实体的这个类
@Entity
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User() {}
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// getters and setters: ~30 extra lines of code
}
这是一个相当简单的类,但想象一下如果我们添加了 getter 和 setter 的额外代码。我们会得到一个定义,其中包含比相关业务信息更多的样板零值代码:“用户具有名字和姓氏以及年龄。”
现在让我们 Lombok-ize 这个类
@Entity
@Getter @Setter @NoArgsConstructor // <--- THIS is it
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
通过添加 @Getter 和 @Setter 注解,我们告诉 Lombok 为所有类字段生成这些代码。@NoArgsConstructor 将导致生成一个空构造函数。
请注意,这是整个类代码;与带有 // getters and setters 注释的上方版本不同,我们没有省略任何内容。对于一个具有三个相关属性的类,这节省了大量的代码!
如果我们向 User 类添加属性(属性),将会发生同样的情况;我们应用注解到类型本身,以便它们默认会处理所有字段。
如果我们想要细化某些属性的可见性怎么办?例如,如果由于预计它们将被读取,但不由应用程序代码显式设置,我们想要使我们的实体的 id 字段修饰符可见于 package 或 protected,我们可以只对该特定字段使用更细粒度的 @Setter
private @Id @Setter(AccessLevel.PROTECTED) Long id;
5. 延迟 Getter
应用程序通常需要执行代价高昂的操作并保存结果以供后续使用。
例如,假设我们需要从文件或数据库读取静态数据。通常,良好的做法是只检索一次这些数据,然后将其缓存,以便应用程序可以在内存中读取。这可以避免应用程序重复执行代价高昂的操作。
另一种常见模式是在第一次需要时才检索这些数据。换句话说,我们只在调用相应的 getter 方法第一次时才获取数据。我们称之为延迟加载。
假设这些数据被缓存为一个类的字段。该类现在必须确保对该字段的任何访问都返回缓存的数据。实现此类的一种可能方式是使用 getter 方法,仅当该字段为null时才检索数据。我们称之为延迟 getter。
Lombok 通过我们在上面看到的 @Getter 注解中的 lazy 参数使这成为可能。
例如,考虑这个简单的类
public class GetterLazy {
@Getter(lazy = true)
private final Map<String, Long> transactions = getTransactions();
private Map<String, Long> getTransactions() {
final Map<String, Long> cache = new HashMap<>();
List<String> txnRows = readTxnListFromFile();
txnRows.forEach(s -> {
String[] txnIdValueTuple = s.split(DELIMETER);
cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1]));
});
return cache;
}
}
这会从文件中读取一些交易记录到Map中。由于文件中的数据不会更改,我们将一次缓存它,并通过 getter 允许访问。
如果现在查看这个类的编译代码,我们会看到一个 getter 方法,如果缓存是null,则会更新缓存,然后返回缓存的数据
public class GetterLazy {
private final AtomicReference<Object> transactions = new AtomicReference();
public GetterLazy() {
}
//other methods
public Map<String, Long> getTransactions() {
Object value = this.transactions.get();
if (value == null) {
synchronized(this.transactions) {
value = this.transactions.get();
if (value == null) {
Map<String, Long> actualValue = this.readTxnsFromFile();
value = actualValue == null ? this.transactions : actualValue;
this.transactions.set(value);
}
}
}
return (Map)((Map)(value == this.transactions ? null : value));
}
}
值得指出的是,Lombok 将数据字段包装在AtomicReference 中。 这确保了对 transactions 字段的原子更新。getTransactions() 方法还确保在 transactions 为null时读取文件。
我们不鼓励直接从类中访问 AtomicReference transactions 字段。我们建议使用getTransactions() 方法来访问该字段。
因此,如果我们在同一个类中使用另一个 Lombok 注解,例如ToString,它将使用getTransactions() 而不是直接访问该字段。
6. 单字段修改的不可变 Setter
此外,我们可以使用@With 注解自动生成一个方法,该方法可以克隆一个只有一个字段更改的对象。例如,当我们要克隆一个带有新age值的User对象时,我们可以用@With注解该字段
@AllArgsConstructor
public class User implements Serializable {
private @Id Long id;
private final String firstName;
private final String lastName;
@With private final int age;
}
随后,我们可以使用自动生成的withAge(int newAge) 实例方法来克隆一个User,但使用不同的年龄
User user = new User("John","Smith",40);
User user_updated = user.withAge(41);
7. 值类/DTO
在许多情况下,我们希望定义一种数据类型,其唯一目的是表示复杂的“值”作为“数据传输对象”,。大多数时候以我们构建一次并且不想更改的不可变数据结构的形式。
我们设计一个类来表示成功的登录操作。我们希望所有字段都是非空的,并且对象是不可变的,以便我们可以线程安全地访问它们的属性
public class LoginResult {
private final Instant loginTs;
private final String authToken;
private final Duration tokenValidity;
private final URL tokenRefreshUrl;
// constructor taking every field and checking nulls
// read-only accessor, not necessarily as get*() form
}
再次强调,我们为注释部分编写的代码量将比我们想要封装的信息所需要的代码量要大得多。我们可以使用 Lombok 来改进这一点
@RequiredArgsConstructor
@Accessors(fluent = true) @Getter
public class LoginResult {
private final @NonNull Instant loginTs;
private final @NonNull String authToken;
private final @NonNull Duration tokenValidity;
private final @NonNull URL tokenRefreshUrl;
}
一旦我们添加了@RequiredArgsConstructor 注解,我们将为类中的所有 final 字段获得一个构造函数,就像我们声明的那样。将 @NonNull 添加到属性会使我们的构造函数检查空值,并相应地抛出 NullPointerExceptions。如果字段是非 final 的,并且我们为它们添加了 @Setter,也会发生这种情况。
我们想要为我们的属性使用枯燥的旧 get*() 形式吗?由于我们在此示例中添加了 @Accessors(fluent=true),因此“getter” 将与属性具有相同的方法名; getAuthToken() 简单地变为 authToken()。
这种“流畅”形式也将应用于非 final 字段的属性 setter,并允许进行链式调用
// Imagine fields were no longer final now
return new LoginResult()
.loginTs(Instant.now())
.authToken("asdasd")
// and so on
8. 核心 Java 样板代码
当我们生成 toString()、equals() 和 hashCode() 方法时,我们最终需要编写和维护代码的另一个情况。 IDE 尝试通过提供自动生成这些方法的模板来提供帮助,以便基于我们的类属性进行操作。
我们可以使用其他 Lombok 类级注解来自动化此过程
- @ToString:将生成一个 toString() 方法,其中包含所有类属性。无需自己编写并维护它,因为我们丰富了数据模型。
- @EqualsAndHashCode:默认情况下,将生成 equals() 和 hashCode() 方法,同时考虑所有相关字段,并根据 经过深思熟虑的语义。
这些生成器提供了非常方便的配置选项。例如,如果我们的注释类参与了层次结构,我们可以只使用 callSuper=true 参数,父结果将在生成方法代码时被考虑。
8.1. 一个演示
为了演示这一点,假设我们的 User JPA 实体示例包含对与该用户关联的事件的引用
@OneToMany(mappedBy = "user")
private List<UserEvent> events;
我们不希望每次调用 User 的 toString() 方法时都转储整个事件列表,仅仅因为我们使用了 @ToString 注解。相反,我们可以像这样对其进行参数化, @ToString(exclude = {“events”}),这样就不会发生。这也有助于避免循环引用,例如,如果 UserEvent 具有对 User 的引用。
对于 LoginResult 示例,我们可能只想根据 token 本身来定义相等性和哈希码计算,而不是类中的其他 final 属性。然后,我们可以简单地写成 @EqualsAndHashCode(of = {“authToken”})。
如果到目前为止我们所审查的注解中的特性引起了您的兴趣,我们还可能需要检查 @Data 和 @Value 注解,因为它们的行为就像将一组注解应用于我们的类一样。毕竟,这些讨论过的用法在许多情况下经常被放在一起。
8.2. (不) 在 JPA 实体中使用 @EqualsAndHashCode
是否应该使用默认的 equals() 和 hashCode() 方法,或者为 JPA 实体创建自定义方法,是开发人员经常讨论的话题。我们可以遵循 多种方法,每种方法都有其优缺点。
默认情况下,@EqualsAndHashCode 包含实体类中的所有非 final 属性。 我们可以尝试通过使用 @EqualsAndHashCode 的 onlyExplicitlyIncluded 属性,让 Lombok 仅使用实体的 primary key 来“修复”此问题。尽管如此,生成的 equals() 方法可能会导致一些问题。Thorben Janssen 在 他的博客文章之一 中更详细地解释了这种情况。
总的来说,我们应该避免使用 Lombok 生成 JPA 实体中的 equals() 和 hashCode() 方法。
9. Builder 模式
以下内容可以作为 REST API 客户端的示例配置类
public class ApiClientConfiguration {
private String host;
private int port;
private boolean useHttps;
private long connectTimeout;
private long readTimeout;
private String username;
private String password;
// Whatever other options you may thing.
// Empty constructor? All combinations?
// getters... and setters?
}
我们可以采用一种初始方法,基于使用类默认的空构造函数,并为每个字段提供 setter 方法;然而,我们理想情况下希望配置在构建(实例化)后不再被重置,从而使其成为不可变的。因此,我们希望避免使用 setter,但编写这样一个潜在的冗长参数构造函数是一种反模式。
相反,我们可以告诉工具生成一个 builder 模式,这避免了我们编写额外的 Builder 类和相关的流畅 setter 类方法,只需在我们的 ApiClientConfiguration 上添加 @Builder 注解即可:
@Builder
public class ApiClientConfiguration {
// ... everything else remains the same
}
将类定义保留如上(不声明构造函数或 setter + @Builder),我们可以将其用作
ApiClientConfiguration config =
ApiClientConfiguration.builder()
.host("api.server.com")
.port(443)
.useHttps(true)
.connectTimeout(15_000L)
.readTimeout(5_000L)
.username("myusername")
.password("secret")
.build();
10. 已检查异常的负担
许多 Java API 被设计为抛出一个或多个已检查异常;客户端代码被迫要么捕获异常,要么声明抛出异常。我们有多少次将我们知道不会发生的这些异常变成如下代码:
public String resourceAsString() {
try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException | UnsupportedCharsetException ex) {
// If this ever happens, then its a bug.
throw new RuntimeException(ex); <--- encapsulate into a Runtime ex.
}
}
如果我们想避免这种代码模式,因为编译器会不高兴(并且我们知道这些已检查错误不会发生),可以使用恰如其分的 @SneakyThrows
@SneakyThrows
public String resourceAsString() {
try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
return br.lines().collect(Collectors.joining("\n"));
}
}
11. 确保释放我们的资源
Java 7 引入了 try-with-resources 块,以确保由实现 java.lang.AutoCloseable 实例持有的资源在退出时被释放。
Lombok 提供了一种替代且更灵活的方式来实现这一点,即通过 @Cleanup。 我们可以将其用于任何我们想要确保释放其资源的局部变量。它们不需要实现任何特定的接口,我们将只是调用 close() 方法
@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");
我们的释放方法有不同的名称?没问题,我们只需自定义注解即可
@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");
12. 注解我们的类以获取 Logger
我们中的许多人通过创建我们选择的框架的 Logger 实例,在我们的代码中谨慎地添加日志语句。 假设 SLF4J
public class ApiClientConfiguration {
private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);
// LOG.debug(), LOG.info(), ...
}
这是一种非常常见的模式,Lombok 开发人员已经为我们简化了它
@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {
// log.debug(), log.info(), ...
}
许多 日志框架 都受支持,当然,我们可以自定义实例名称、主题等。
13. 编写线程安全的方法
在 Java 中,我们可以使用 synchronized 关键字来实现临界区;但是,这并不是一种 100% 安全的方法。 其他客户端代码最终也可能同步到我们的实例,从而可能导致意外的死锁。
这就是 @Synchronized 的用武之地。 我们可以使用它来注解我们的方法(实例方法和静态方法),我们将获得一个自动生成的、私有的、未暴露的字段,我们的实现将使用它来进行锁定:
@Synchronized
public /* better than: synchronized */ void putValueInCache(String key, Object value) {
// whatever here will be thread-safe code
}
但是,在使用 Java 21 引入的 虚拟线程 时,我们应该使用 @Locked、@Locked.Read 和 @Locked.Write 注解来获取一个 ReentrantLock。
14. 自动化对象组合
Java 没有语言级别的构造来平滑“优先组合继承”的方法。其他语言具有内置的概念,如 Traits 或 Mixins,以实现此目的。
Lombok 的 @Delegate 在我们想要使用这种编程模式时非常有用。 让我们考虑一个示例,其中我们
- 希望用户和客户在姓名和电话号码方面共享一些通用属性
- 为这些字段定义一个接口和一个适配器类
- 让我们的模型实现该接口并@Delegate到它们的适配器,有效地将它们组合到我们的联系信息中
首先,让我们定义一个接口
public interface HasContactInformation {
String getFirstName();
void setFirstName(String firstName);
String getFullName();
String getLastName();
void setLastName(String lastName);
String getPhoneNr();
void setPhoneNr(String phoneNr);
}
现在,一个适配器作为支持类
@Data
public class ContactInformationSupport implements HasContactInformation {
private String firstName;
private String lastName;
private String phoneNr;
@Override
public String getFullName() {
return getFirstName() + " " + getLastName();
}
}
现在,最有趣的部分是,让我们看看将联系信息组合到两个模型类中有多么容易
public class User implements HasContactInformation {
// Whichever other User-specific attributes
@Delegate(types = {HasContactInformation.class})
private final ContactInformationSupport contactInformation =
new ContactInformationSupport();
// User itself will implement all contact information by delegation
}
客户的情况将非常相似,为了简洁起见,我们可以省略示例。
15. 字段名称常量
当我们处理具有许多字段的类时,尤其是在使用反射、序列化框架或构建动态查询时,我们经常创建表示字段名称的字符串常量。手动管理这些常量容易出错且重复。
Lombok 通过提供@FieldNameConstants 注释来简化此任务,该注释会自动为类的字段名称生成字符串常量。
让我们考虑以下类
@Getter
@FieldNameConstants
public class Person {
private final String firstName;
private final String lastName;
private final int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
当我们使用@FieldNameConstants 注释时,Lombok 会生成一个方便的嵌套静态类,其中包含常量字段名称。这使我们能够安全地引用字段并避免在代码中硬编码字符串字面量。
例如,Lombok 会生成一个嵌套类 Fields,其中包含常量
public class Person {
public static final class Fields {
public static final String firstName = "firstName";
public static final String lastName = "lastName";
public static final String age = "age";
// Existing code
}
现在,每当我们需要按名称引用字段时,我们都可以使用这些常量,从而避免拼写错误并简化重构
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
query.select(root).where(cb.equal(root.get(Person.Fields.lastName), "Doe"));
此功能有助于我们避免代码中散布的字符串字面量,使其更安全、更易读且更易于维护。
我们还可以自定义内部类的名称
@FieldNameConstants(innerTypeName = "FieldConstants")
16. 回滚 Lombok?
简短回答:完全不需要。
可能会担心,如果我们在一个项目中使用了 Lombok,我们可能会想以后撤销该决定。潜在的问题可能是,有大量的类被注解用于它。在这种情况下,得益于同一项目的delombok 工具,我们得到了保障。
通过delombok-ing 我们的代码,我们获得了自动生成的 Java 源代码,其中具有与 Lombok 构建的字节码完全相同的功能。然后,我们可以简单地用这些新的delomboked 文件替换我们原始的注解代码,从而不再依赖它。
我们可以将此功能集成到我们的构建中。
17. 结论
本文中我们还没有介绍其他一些功能。我们可以更深入地了解功能概述以获取更多详细信息和用例。
此外,我们展示的大多数功能都有许多自定义选项,我们可能会觉得很有用。可用的内置配置系统也可能对我们有所帮助。
现在,我们可以让 Lombok 有机会进入我们的 Java 开发工具集中,从而提高我们的生产力。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
















