Lombok技术点

2023年 9月 27日 65.6k 0

避免重复的代码

1. Lombok的介绍

Java是一种很棒的编程语言,但有时候在我们处理常见任务或遵循某些框架实践时,可能会变得过于冗长。这往往对我们程序的业务部分没有任何实际价值,这就是Lombok发挥作用的地方,它可以提高我们的生产效率。

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

将其包含在我们使用的任何系统的构建中非常简单。Project Lombok的项目页面上有关于具体操作的详细说明。我的大多数项目都是基于Maven的,所以我通常只需在提供的范围中添加它们的依赖项即可开始使用:


    ...
    
        org.projectlombok
        lombok
        1.18.20
        provided
    
    ...

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

请注意,依赖于Lombok不会使我们的.jars文件的用户也依赖于它,因为它是纯粹的构建依赖关系,而不是运行时依赖关系。

2. Getter/Setter和构造函数 - 多么重复

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

这是如此常见,以至于大多数IDE都支持自动生成这些模式(以及更多)。然而,这些代码需要存在于我们的源代码中,并在添加新属性或重命名字段时进行维护。

让我们考虑这个作为JPA实体类使用的类:

@Entity
public class User implements Serializable {

    private @Id Long id; // 在持久化时设置

    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 //  {
            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 transactions = new AtomicReference();

    public GetterLazy() {
    }

    //other methods

    public Map getTransactions() {
        Object value = this.transactions.get();
        if (value == null) {
            synchronized(this.transactions) {
                value = this.transactions.get();
                if (value == null) {
                    Map 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()而不是直接访问字段。

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注解,我们将获得一个构造函数,用于类中的所有final字段,就像我们声明的那样。将@NonNull添加到属性中会使我们的构造函数检查是否为空,并相应地抛出NullPointerException。如果字段是非final的,并且我们为它们添加了@Setter注解,也会发生这种情况。

我们是否希望为属性使用传统的get*()形式?由于我们在本例中添加了@Accessors(fluent=true),因此“getter”方法将具有与属性相同的方法名;getAuthToken()只需变为authToken()。

这种“fluent”形式也适用于非final字段的属性设置器,并允许进行链式调用:

// 假设字段现在不再是final的
return new LoginResult()
.loginTs(Instant.now())
.authToken("asdasd")
. // 等等

5. 核心Java样板代码

另一个我们需要编写并维护的代码是生成toString()、equals()和hashCode()方法的情况。IDE会尝试通过模板自动生成这些方法,根据我们的类属性来生成。

我们可以通过其他Lombok类级别的注解来自动化这个过程:

  • @ToString: 会生成包含所有类属性的toString()方法。我们不需要自己编写和维护它,当我们丰富我们的数据模型时。
  • @EqualsAndHashCode: 默认情况下,根据所有相关字段生成equals()和hashCode()方法,并根据非常完善的语义进行生成。

这些生成器提供了非常方便的配置选项。例如,如果我们的注解类是层级结构的一部分,

默认情况下,@EqualsAndHashCode会包括实体类的所有非final属性。我们可以尝试使用@EqualsAndHashCode的onlyExplicitlyIncluded属性来“修复”这个问题,使Lombok仅使用实体的主键。然而,生成的equals()方法可能会引发一些问题。Thorben Janssen在他的博客文章中更详细地解释了这种情况。

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

6. 构建器模式

下面是一个用于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;

    // 其他选项

    // 空构造函数?所有组合?

    // getter... 和 setter?
}

我们可以采用初始方法,使用类的默认空构造函数并为每个字段提供setter方法;然而,我们理想情况下希望配置在被构建(实例化)后不可重新设置,从而使其成为不可变对象。因此,我们希望避免使用setter,但是编写这样一个可能很长的args构造函数是一种反模式。

相反,我们可以使用@Builder注解告诉工具生成一个构建器模式,这样我们就不需要编写额外的Builder类和相关的类似setter的方法,只需将@Builder注解添加到我们的ApiClientConfiguration类中:

@Builder
public class ApiClientConfiguration {

    // ... 其他部分保持不变

}

将类定义保持如上所示(不声明构造函数或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设计为可以抛出多个受检异常;客户端代码被要求要么捕获这些异常,要么声明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) {
// 如果发生这种情况,那么这是一个bug。
throw new RuntimeException(ex);

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论