1. 避免重复代码

Java 很好,但如果我们墨守成规遵守某些设计规范,它有时会变得过于冗长。这通常不会给我们程序的业务方面带来任何实际价值,而这正是 Lombok 可以提高我们工作效率的地方。

Lombok 的工作原理是根据代码注解,在构建过程中自动生成 Java 字节码到我们的 .class 文件中。

有关于具体细节的详细说明,请参见 Lombok 的 项目主页

Lombok 使用非常简单,对于Maven项目需要引入下面的依赖:

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

最新版本可去官网查询。

2. 自动生成 Getters/Setters和构造器

在Java世界里,Getter、Setter 方法是在普通不过的家常便饭。虽然大多数IDE都提供了自动生成代码的功能,但字段多了之后显得非常冗余,且后期修改还是需要自己维护。

例如下面的实体类:

@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 和 setters: 大约30行
}

这个类很简单,但我们需要为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会自动为我们生成getter/setter方法, @NoArgsConstructor 生成一个空的无参构造器。

如果想修改某些属性的可见性怎么办?例如,保持id字段仅package 或 protected 可见,不希望被明确设置,但可以被读取:

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

3. 懒加载 - Getter

开发中,我们通会遇到一些耗时操作,例如从文件或数据库中读取数据。通常做法是初始化一次,然后将其缓存起来供后续请求使用,避免重复执行这些耗时的操作。

另一种是在第一次需要访问数据时才去获取,这种模式称为延迟加载(lazy-loading)。

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 方法,如果缓存为空,则更新缓存,然后返回缓存的数据:

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字段的原子更新。如果transactions为null,getTransactions() 方法还会确保读取文件。

我们不建议直接使用 AtomicReference transactions 字段。而是使用 getTransactions() 方法来访问该字段。

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

4. DTO 类

定义 LoginResult 表示登录成功,我们希望所有字段都是非空的,并且对象是不可变的,以便我们可以在线程安全的情况下访问其属性

public class LoginResult {

    private final Instant loginTs;

    private final String authToken;
    private final Duration tokenValidity;
    
    private final URL tokenRefreshUrl;

    // 构造函数,接收每个字段并检查是否为null

    // 只读访问器,不一定是get*()形式
}

同样,我们需要为注释部分编写的代码量将比我们想要封装的信息量大得多。我们可以使用 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异常。

使用 @Accessors(fluent=true) 注解,设置getter方法名和属性名一样,而不是 get*() 的形式。例如getAuthToken()变成了authToken().

"fluent" 只适用于非final字段,并允许链式调用:

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

5. Core Java Boilerplate

Another situation where we end up writing code we need to maintain is when generating the toString(), equals() and hashCode() methods. IDEs try to help with templates for auto-generating these in terms of our class attributes.

We can automate this by means of other Lombok class-level annotations:

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

假设我们的User JPA实体包含与该用户相关的Event的引用:

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

我们不希望调用User toString()方法时包含事件列表,那我们就可以这样设置参数 @ToString(exclude = {“events”})。

对于 LoginResult类,判断两个对象是否相对,我们希望仅根据 token 本身而不是类中的其他final属性来定义equals和hashCode计算,可以这样设置 @EqualsAndHashCode(of = {“authToken”} 的内容。

还有其他一些注解如 @Data@Value 的用法请参考官方文档,这里就不展开细说。

5.1. JPA使用@EqualsAndHashCode

对于JPA实体,是否应该使用默认的 equals()hashCode() 方法,还是创建自定义实现,是一个比较有争议的话题。这里有常见的 几种方法,每种方法都有其优缺点。

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

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

6. Builder 设计模式

下面是一个简单的配置类:

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
public class ApiClientConfiguration {

    // ... everything else remains the same

}

使用 @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 中我们需要捕获很多异常。

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

如果是释放方法的函数名不是 “close”,通过设置参数指定即可:

@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");

9. 日志注解

通过Lombok我们可以很方便的获取Log实例,它支持很多日志框架。例如SLF4J:

public class ApiClientConfiguration {

    private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);

    // LOG.debug(), LOG.info(), ...

}

使用 @Slf4j 注解后,代码简化为:

@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {

    // log.debug(), log.info(), ...

}

10. 编写线程安全的方法

在 Java 中,我们可以使用 synchronized 实现线程安全,然而可能会导致死锁问题。

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

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

11. Automate Objects Composition

Java doesn’t have language level constructs to smooth out a “favor composition inheritance” approach. Other languages have built-in concepts such as Traits or Mixins to achieve this.

Lombok’s @Delegate comes in very handy when we want to use this programming pattern. Let’s consider an example:

  • We want Users and Customers to share some common attributes for naming and phone number.
  • We define both an interface and an adapter class for these fields.
  • We’ll have our models implement the interface and @Delegate to their adapter, effectively composing them with our contact information.

First, let’s define an interface:

public interface HasContactInformation {

    String getFirstName();
    void setFirstName(String firstName);

    String getFullName();

    String getLastName();
    void setLastName(String lastName);

    String getPhoneNr();
    void setPhoneNr(String phoneNr);

}

Now an adapter as a support class:

@Data
public class ContactInformationSupport implements HasContactInformation {

    private String firstName;
    private String lastName;
    private String phoneNr;

    @Override
    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }
}

Now for the interesting part; see how easy it is to compose contact information into both model classes:

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
    
}

The case for Customer would be so similar that we can omit the sample for brevity.

12. Rolling Lombok Back?

Short answer: Not at all really.

There may be a worry that if we use Lombok in one of our projects, we may later want to rollback that decision. The potential problem could be that there are a large number of classes annotated for it. In this case, we’re covered thanks to the delombok tool from the same project.

By delombok-ing our code, we get auto-generated Java source code with exactly the same features from the bytecode Lombok built. We can then simply replace our original annotated code with these new delomboked files, and no longer depend on it.

This is something we can integrate in our build.

13. 总结

There are still some other features we haven’t presented in this article. We can take a deeper dive into the feature overview for more details and use cases.

Furthermore, most functions we’ve shown have a number of customization options that we may find handy. The available built-in configuration system could also help us with that.

Now that we can give Lombok a chance to get into our Java development tool set, we can boost our productivity.

The example code can be found in the GitHub project.