慎用Lombok的@Builder注解:弊端与替代方案

原文标题:请谨慎使用 @Builder 注解!

原文作者:阿里云开发者

冷月清谈:

Lombok的@Builder注解虽然能实现链式编程,但并非最佳实践。它存在以下问题:无法区分必填和可选参数,生成的构造器可变且不适用于短暂对象,不能处理抽象类型参数,继承关系时需要@SuperBuilder,设置默认值需要@Builder.Default。文章建议使用@Accessors(chain = true)代替@Builder,实现更简洁、安全的链式编程。

怜星夜思:

1、除了文章提到的@Accessors,还有哪些方法可以实现链式编程?
2、使用@Builder注解时,如何避免默认值失效问题?
3、@Builder注解的适用场景有哪些?

原文内容

阿里妹导读


本文主要指出 @Builder 存在的一些问题,指出它并不是链式编程的最佳实践。

一、前言

前一段时间写过一篇 《使用 lombok @Builder 注解,设置默认值时要千万小心!》 的文章,文章提到使用 @Builder 注解将会导致设置的默认值失效问题。
最近读了一篇文章:《Oh !! Stop using @Builder》[1]也颇受启发,发现很多人的确被 @Builder 注解的名字误导了。
大多数同学使用 @Builder 无非就是为了链式编程,然而 @Builder 并不是链式编程的最佳实践,它会额外创建内部类,存在继承关系时还需要使用 @SuperBuilder 注解[2],设置默认值时也需要额外的 @Builder.Default 去设置默认值[3],无疑增加了很多不必要的复杂度。
有些同学可能说如果你把这些问题都了解就不会遇到这些坑,没必要“因噎废食”,可是这些问题已经让无数人一次次趟坑,说明它并不是最佳实践,如果有更简便办法可以实现链式编程,又何必徒增这么多复杂度呢?
其实很多人选择使用 @Builder 实现链式编程,无非是它“更常见”和“最熟悉” !

二、为什么?

(1)@Builder 生成的构造器不是完美的,它不能区分哪些参数是必须的,哪些是可选的。如果没有提供必须的参数,构造器可能会创建出不完整或者不合法的对象。
可以看第三部分的例子, @Builder 注解产生的 Builder 类的构造方法默认并不能限定必传参数。
(2)很多人 喜欢 @Builder 和 @Data 搭配使用,导致生成的构造器是可变的,它允许使用 setter 方法修改构造器的状态。这违反了构造器模式的原则,构造器应该是不可变的,一旦创建就不能被修改。
如果非要使用 @Builder ,那么不要用 @Data ,要用 @Getter。相对来说,反而 @Accessors 的行为更符合这个要求。
(3)@Builder 生成的构造器不适合用于短暂的对象,它会增加代码的复杂度和冗余。构造器模式更适合用于生命周期较长、有多种变体的对象。
实际使用中经常发现 @Builder 滥用的情况,有些仅仅一两个属性的类也都要用 @Builder,真的没必要用,直接用全参的构造方法都比这更简洁。
(4)@Builder 生成的构造器不能处理抽象类型的参数,它只能接受具体类型的对象。这限制了构造器的灵活性和扩展性,不能根据不同的需求创建不同风格的对象。
(5)继承关系时,子类需要使用 @SuperBuilder。对象继承后,子类的 Builder 因为构造函数的问题,使用不当大概率会报错,并且无法设置父类的属性,还需要使用 @SuperBuilder 来解决问题。
(6)设置默认值需要使用 @Builder.Default。很容易因为对此不了解,导致默认值不符合预期导致出现 BUG。

三、怎么做?

正如 《Oh !! Stop using @Builder》 所推荐的一样,建议使用  @Accessors(chain = true) 来代替。


3.1 不使用 @Builder

package io.gitrebase.demo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {

private T payload;

private Status status;

}

使用示例:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(assignableTypes = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {

@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error(“Unhandled Exception”, exception);
Status status = new Status();
status.setResponseCode(“RESPONSE_CODE_IDENTIFIER”);
status.setDescription(“Bla Bla Bla”);
APIResponse response = new APIResponse();
response.setStatus(status);
return response;
}

}


3.2 使用 @Builder

package io.gitrebase.demo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {

private T payload;

private Status status;

}

等价代码:

package io.gitrebase.demo;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
// Make constructor private so object cannot be created outside the class
// Allowing only PersonBuilder.build to create an instance of the class
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Person {

private String firstName;
private String lastName;
private String email;
private String mobile;

// PersonBuilder can only be created with firstName, lastName
// which implies that Person object ll always be instantiated with firstName & lastName
public static PersonBuilder builder(String firstName, String lastName) {
return new PersonBuilder(firstName, lastName);
}

public static class PersonBuilder {

private final String firstName;
private final String lastName;
private String email;
private String mobile;

// Make constructor private so object cannot be created outside the class
// Allowing only Person.builder to create an instance of PersonBuilder
private PersonBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public PersonBuilder email(String email) {
this.email = email;
return this;
}

public PersonBuilder mobile(String mobile) {
this.mobile = mobile;
return this;
}

public Person build() {
return new Person(firstName, lastName, email, mobile);
}

}

}

使用示例:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {

@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error(“Unhandled Exception”, exception);
return APIResponse.builder().status(Status.builder()
.responseCode(“RESPONSE_CODE_IDENTIFIER”)
.description(“Bla Bla Bla”)
.build())
.build();
}

}


3.3 使用 @Accessors 注解

package io.gitrebase.demo;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class APIResponse<T> {

private T payload;

private Status status;

}

等价代码:

package io.gitrebase.demo;

import lombok.experimental.Accessors;

@Accessors(chain = true)
public class APIResponse<T> {

private T payload;

private Status status;

public T getPayload() {
return this.payload;
}

public APIResponse<T> setPayload(T payload) {
this.payload = payload;
return this;
}

public Status getStatus() {
return this.status;
}

public APIResponse<T> setStatus(Status status) {
this.status = status;
return this;
}
}

使用示例:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {

@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public APIResponse handleException(Exception exception) {
log.error(“Unhandled Exception”, exception);
var status = new Status().setResponseCode(“RESPONSE_CODE_IDENTIFIER”).setDescription(“Bla Bla Bla”);
return new APIResponse().setStatus(status);
}

}

原文中给的示例相对简单,实际运用时,如果属性较多,且分为必传属性和选填属性时,可以将必传参数定义在构造方法中,加上 @Accessors 注解,这样就可以实现必传参数的传入,又可以实现选填参数的链式调用。

假设 Student 类,它的 学生ID和年级和班级是必填的,姓名、性别、住址是选填的,那么示例代码如下:
import lombok.Accessors;

// 使用 @Accessors 注解,设置 chain = true,生成返回 this 的 setter 方法
@Accessors(chain = true)
public class Student {
// 定义必传属性,使用 final 修饰,不提供 setter 方法
private final int studentId; // 学生ID
private final int grade; // 年级
private final int classNum; // 班级

// 定义选填属性,提供 setter 方法
private String name; // 姓名
private String gender; // 性别
private String address; // 住址

// 定义构造方法,接收必传参数
public Student(int studentId, int grade, int classNum) {
this.studentId = studentId;
this.grade = grade;
this.classNum = classNum;
}

// 省略 getter 和 setter 方法
}

// 使用示例
Student student = new Student(1001, 3, 8) // 创建一个学生对象,传入必传参数
.setName(“张三”) // 设置姓名
.setGender(“男”) // 设置性别
.setAddress(“北京市朝阳区”); // 设置住址

另外该注解还有很多设置项,感兴趣可以进一步研究:

/**
* A container for settings for the generation of getters, setters and "with"-ers.
* <p>
* Complete documentation is found at <a href="https://projectlombok.org/features/experimental/Accessors">the project lombok features page for &#64;Accessors</a>.
* <p>
* Using this annotation does nothing by itself; an annotation that makes lombok generate getters, setters, or "with"-ers
* such as {@link lombok.Setter} or {@link lombok.Data} is also required.
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
/**
* If true, accessors will be named after the field and not include a {@code get} or {@code set}
* prefix. If true and {@code chain} is omitted, {@code chain} defaults to {@code true}.<br>
* NB: This setting has no effect on {@code @With}; they always get a "with" prefix.<br>
* <strong>default: false</strong>
*
* @return Whether or not to make fluent methods (named {@code fieldName()}, not for example {@code setFieldName}).
*/
boolean fluent() default false;

/**

  • If true, setters return {@code this} instead of {@code void}.
  • <strong>default: false</strong>, unless {@code fluent=true}, then <strong>default: true</strong>
  • @return Whether or not setters should return themselves (chaining) or {@code void} (no chaining).
    */
    boolean chain() default false;

/**

  • If true, generated accessors will be marked {@code final}.
  • <strong>default: false</strong>
  • @return Whether or not accessors should be marked {@code final}.
    */
    boolean makeFinal() default false;

/**

  • If present, only fields with any of the stated prefixes are given the getter/setter treatment.
  • Note that a prefix only counts if the next character is NOT a lowercase character or the last
  • letter of the prefix is not a letter (for instance an underscore). If multiple fields
  • all turn into the same name when the prefix is stripped, an error will be generated.
  • @return If you are in the habit of prefixing your fields (for example, you name them {@code fFieldName}, specify such prefixes here).
    */
    String prefix() default {};
    }


3.4 使用 IDEA 默认 Setter 生成功能

如果有些同学担心 @Accessors 在 lombok.experimental 包不够“稳定”,可以参考  @Accessors 的功能自己写一份即可。如果嫌麻烦可以调用 Generate 快捷键(mac 上 Command +N)选择生成 Getter、Setter ,其中 Setter 选择 Builder 即可实现 set 方法返回 this 的效果,干净纯粹,无其他依赖!

四、启发

有些同学认为如果能够深入了解 @Builder 注解的每个细节,包括如何正确设置默认值,那么就不容易出错。可是这无疑增加了额外的复杂度,我们常见的使用 @Builder 注解无非就是为了链式编程,而 @Accessors 就可以轻松实现这个效果而且还可以避免底层创建一个 Builder 对象,也可以避免默认值的坑,而且当类存在继承关系时,还需要使用 @SuperBuilder 注解,问题的复杂度又提高了。所以,何必执着于使用 @Builder 注解呢?
本文主要指出 @Builder 存在的一些问题,指出它并不是链式编程的最佳实践。但如果你对 @Builder 了如指掌,情有独钟,继续使用无可厚非,如果你认为这的确徒增了很多不必要的复杂度,可以考虑使用 @Accessors 或者自己手动改造 Setter 方法。
大家在使用 lombok 注解时,必须对每个注解的效果非常了解,推荐大家主动查看编译后的代码,也可以使用 JClaslib 等插件查看对应的字节码,避免因为理解偏差造成误用。
另外,“大家都这么用”、“工具库提供的功能” 并不意味着就是对的(否则很多知名工具类库中也不会有各种 @Deprecated 标识的过期类了)或者是“对的” 但不是最好的。大家在使用某个功能时要独立思考和判断能力,如果不合理可以进行改进。

参考链接:

[1]https://medium.com/gitrebase/oh-stop-using-builder-9061a5911d8c

[2]https://www.baeldung.com/lombok-builder-inheritance

[3]https://www.baeldung.com/lombok-builder-default-value

阿里云开发者社区,千万开发者的选择


阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。

确保在Builder类中显式调用setter方法设置默认值,即使字段已经有了默认值。

@Builder注解适用于构建复杂的、生命周期较长的对象,尤其是那些具有多个可选属性的对象。

如果你需要构建不可变对象,@Builder注解可以帮助你实现。

当需要构建的对象具有复杂的继承关系时,@Builder注解可以简化代码。

如果可以,尽量避免使用默认值,而是通过构造函数或setter方法显式设置所有属性的值。

一种方法是自己手动修改setter方法,使其返回this,从而实现链式调用。

Kotlin语言天生支持链式调用,如果你正在考虑学习一门新语言,Kotlin是个不错的选择。

还可以使用一些第三方库,比如Apache Commons Chain,它提供了更强大的链式编程功能。

可以使用@Builder.Default注解显式设置默认值。