Contents

Lombok 简介

1.避免重复代码

Java 是一门很棒的语言,但对于我们必须在代码中执行的常见任务或遵守某些框架实践来说,它有时会变得过于冗长。这通常不会为我们项目的业务方面带来任何真正的价值,而这正是 Lombok 的用武之地,旨在提高我们的工作效率。

它的工作方式是插入我们的构建过程,并根据我们在代码中引入的许多项目注解将 Java 字节码自动生成到我们的*.class*文件中。

将它包含在我们的构建中,无论我们使用哪个系统,都非常简单。Project Lombok 的项目页面 有详细说明。我的大多数项目都是基于 maven 的,所以我通常只是将它们的依赖项放在provided的范围内,我很高兴:

<dependencies>
    ...
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <scope>provided</scope>
    </dependency>
    ...
</dependencies>

我们可以在这里 查看最新的可用版本。

请注意,依赖 Lombok 不会让我们的*.jar*的用户也依赖它,因为它是纯粹的构建依赖,而不是运行时。

2. Getters/Setters, Constructors – 如此重复

通过公共 getter 和 setter 方法封装对象属性在 Java 世界中非常普遍,许多框架广泛依赖这种“Java Bean”模式(一个具有空构造函数的类和“属性”的 get/set 方法)。

这很常见,以至于大多数 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 *化这个类:

@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*将导致生成一个空的构造函数。

请注意,这是整个类代码,我们没有省略与上面版本不同的任何内容,带有getter 和 setters注解。对于三个相关的属性类,这是一个显着的代码节省!

如果我们进一步向我们的User类添加属性(properties),也会发生同样的情况;我们将注解应用于类型本身,因此默认情况下它们会注意所有字段。

如果我们想优化某些属性的可见性怎么办?例如,如果我们想要保持实体的id字段修饰符packageprotected可见,因为它们应该被读取,但不是由应用程序代码显式设置,我们可以为这个特定字段使用更细粒度的*@Setter :*

private @Id @Setter(AccessLevel.PROTECTED) Long id;

3. 懒惰的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()方法还确保在transactionsnull*时读取文件。

我们不鼓励直接在类中使用AtomicReference 事务字段。*我们建议使用*getTransactions()方法来访问该字段。

出于这个原因,如果我们在同一个类中使用另一个 Lombok 注解,如ToString,它将使用*getTransactions()*而不是直接访问该字段。

4. 价值类/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注解,我们将获得类中所有最终字段的构造函数,就像我们声明它们一样。向属性添加@NonNull会使我们的构造函数检查可空性并相应地抛出NullPointerExceptions*。如果字段不是最终字段并且我们为它们添加了*@Setter* ,也会发生这种情况。

我们是否想要我们的属性使用无聊的旧get()形式?因为我们在这个例子中添加了@Accessors(fluent=true),所以“getters”将与属性具有相同的方法名;getAuthToken()简单地变成authToken()

这种“流畅”的形式将适用于属性设置器的非最终字段,并允许链式调用:

// Imagine fields were no longer final now
return new LoginResult()
  .loginTs(Instant.now())
  .authToken("asdasd")
  . // and so on

5. 核心Java样板

我们最终编写需要维护的代码的另一种情况是在生成toString()、*equals()hashCode()*方法时。IDE 尝试帮助模板根据我们的类属性自动生成这些模板。

我们可以通过其他 Lombok 类级别的注解来自动执行此操作:

  • @ToString :将生成一个包含所有类属性的*toString()*方法。无需自己编写并维护它,因为我们丰富了我们的数据模型。
  • @EqualsAndHashCode :默认情况下将同时生成*equals()hashCode()*方法,考虑所有相关字段,并根据语义非常好

这些生成器提供了非常方便的配置选项。例如,如果我们的注解类属于层次结构的一部分,我们可以只使用callSuper=true参数,并且在生成方法的代码时会考虑父结果。

为了证明这一点,假设我们的User JPA 实体示例包含对与该用户关联的事件的引用:

@OneToMany(mappedBy = "user")
private List<UserEvent> events;

每当我们调用 User 的toString()方法时,我们不希望转储整个事件列表,因为我们使用了@ToString注解。相反,我们可以像这样参数化它,@ToString(exclude = {“events”}),这不会发生。这也有助于避免循环引用,例如,如果UserEvent有对User的引用。

对于LoginResult示例,我们可能只想根据令牌本身而不是我们类中的其他最终属性来定义相等和哈希码计算。然后我们可以简单地编写类似*@EqualsAndHashCode(of = {“authToken”})*的内容。

如果到目前为止我们已经回顾过的注解中的特征是有趣的,我们可能还想检查@Data@Value 注解,因为它们的行为就好像它们中的一组已应用于我们的类。毕竟,在许多情况下,这些讨论的用法非常普遍。

5.1.(不)将*@EqualsAndHashCode*与 JPA 实体一起使用

我们是否应该使用默认的*equals()hashCode()*方法,或者为 JPA 实体创建自定义方法,是开发人员经常讨论的话题。我们可以遵循多种方法 ,每种方法都有其优点和缺点。

**默认情况下,@EqualsAndHashCode包括实体类的所有非最终属性。**我们可以尝试通过使用 @EqualsAndHashCode 的onlyExplicitlyIncluded属性来“修复”这个问题,使 Lombok 只使用实体的主键。尽管如此,生成的*equals()*方法可能会导致一些问题。Thorben Janssen 在他的一篇博客文章 中更详细地解释了这种情况。

一般来说,*我们应该避免使用 Lombok 为我们的 JPA 实体生成*equals()hashCode()方法。

6. 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,但是编写这样一个可能很长的 args 构造函数是一种反模式。

相反,我们可以告诉工具生成一个Builder模式,这使我们不必编写额外的Builder类和关联的类似流利的 setter 方法,只需将 @Builder 注解添加到我们的ApiClientConfiguration

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

7. 检查异常负担

许多 Java API 被设计成可以抛出大量检查异常;客户端代码被迫要么catch要么声明throws。我们有多少次将这些我们知道不会发生的异常变成这样的事情?:

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

8. 确保我们的资源被释放

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

9. 注解我们的类以获得一个记录器

我们中的许多人通过从我们选择的框架中创建一个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(), ...
}

支持很多日志框架 ,当然我们可以自定义实例名称、主题等。

10. 编写线程安全的方法

在Java中,我们可以使用synchronized关键字来实现临界区;但是,这不是 100% 安全的方法。其他客户端代码最终也可以在我们的实例上同步,这可能会导致意外死锁。

这就是@Synchronized 的用武之地。我们可以用它注解我们的方法(实例和静态),我们将获得一个自动生成的、私有的、未公开的字段,我们的实现将使用它来锁定:

@Synchronized
public /* better than: synchronized */ void putValueInCache(String key, Object value) {
    // whatever here will be thread-safe code
}

11.自动化对象组合

Java 没有语言级别的构造来平滑“偏好组合继承”方法。其他语言有内置的概念,例如TraitsMixins来实现这一点。

当我们想要使用这种编程模式时,Lombok 的@Delegate 非常方便。让我们考虑一个例子:

  • 我们希望UserCustomer共享命名和电话号码的一些共同属性。
  • 我们为这些字段定义了一个接口和一个适配器类。
  • 我们将让我们的模型实现接口并*@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
    
}

客户的情况非常相似,为了简洁起见,我们可以省略示例。

12. 回滚 Lombok?

简短的回答:完全没有。

可能会担心,如果我们在其中一个项目中使用 Lombok,我们以后可能想要回滚该决定。潜在的问题可能是为它注解了大量的类。在这种情况下,由于来自同一个项目的delombok工具,我们被覆盖了。

通过对我们的代码进行 delombok 处理,我们获得了自动生成的 Java 源代码,其功能与 Lombok 构建的字节码完全相同。然后,我们可以简单地用这些新的delomboked文件替换我们的原始注解代码,而不再依赖它。

这是我们可以集成到构建 中的东西。