建造者模式:链式Builder设计与Lombok @Builder的陷阱分析
建造者模式:链式Builder设计与Lombok @Builder的陷阱分析
适读人群:中高级Java开发者 | 阅读时长:约20分钟 | 模式类型:创建型
开篇故事
做了这么多年后端,我见过最让人抓狂的代码莫过于"超长构造函数"。有一次接手一个老项目,里面有个 RiskCheckRequest 类,构造函数有14个参数:
new RiskCheckRequest(userId, orderId, amount, currency, channelType,
merchantId, deviceId, ipAddress, userAgent,
geoLocation, riskLevel, timeout, retryCount, extraInfo);我看着这行代码,脑子里浮现了一个问题:第3个参数是 amount 还是 userId?第12个参数是 timeout 还是 retryCount?不看方法签名根本分不清楚。更要命的是,这个类在项目里被调用了几十个地方,每次改动参数顺序都会引发蝴蝶效应,一改就出bug。
那时候引入 Lombok 的 @Builder 注解,感觉像是救星。一个注解就搞定,链式调用,可读性瞬间提升。但没过多久,我们就踩了 Lombok @Builder 的坑——继承时的 Builder 不兼容问题、Jackson 反序列化失败、某些字段默认值被吞掉……一系列问题接踵而至。
今天把建造者模式从原理到 Lombok 的坑,完整梳理一遍。
一、模式动机:解决复杂对象的构建问题
建造者模式(Builder Pattern)适用于以下场景:
- 构造参数过多:当一个类的构造函数有超过4个参数时,就值得考虑 Builder 了。
- 部分参数可选:有些参数有默认值或者可以不设置,用构造函数处理可选参数会产生大量重载。
- 构建过程分步骤:对象的某些属性需要在不同阶段设置,最终一次性构建。
- 同样的过程构建不同的产品:构建步骤固定,但每一步的实现不同,最终产出的对象不同。
建造者模式的核心:将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
二、模式结构
注意:在现代 Java 开发中,Director 角色经常被省略,Builder 通常以流式 API 的形式直接被客户端使用。
三、Spring 源码中的 Builder 分析
3.1 UriComponentsBuilder
Spring MVC 中的 UriComponentsBuilder 是 Builder 模式的经典实现:
UriComponents uriComponents = UriComponentsBuilder
.newInstance()
.scheme("https")
.host("api.example.com")
.port(443)
.path("/v1/users/{userId}/orders")
.queryParam("status", "active")
.queryParam("page", 1)
.buildAndExpand("12345"); // 变量替换
// 输出: https://api.example.com:443/v1/users/12345/orders?status=active&page=1UriComponentsBuilder 的内部实现值得学习:
public class UriComponentsBuilder implements UriBuilder, Cloneable {
private String scheme;
private String ssp;
private String userInfo;
private String host;
private String port;
private CompositePathComponentBuilder pathBuilder;
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
private String fragment;
// 每个setter方法返回this,支持链式调用
public UriComponentsBuilder scheme(String scheme) {
this.scheme = scheme;
return this;
}
public UriComponentsBuilder host(String host) {
this.host = host;
return this;
}
// 最终构建方法
public UriComponents build() {
return build(false);
}
public UriComponents buildAndExpand(Object... uriVariableValues) {
return build().expand(uriVariableValues);
}
}3.2 ResponseEntity.Builder
Spring 5 引入的 ResponseEntity 同样使用了 Builder 模式:
// Builder模式构建HTTP响应
ResponseEntity<ApiResult<UserDTO>> response = ResponseEntity
.status(HttpStatus.OK)
.header("X-Request-Id", requestId)
.header("X-Response-Time", String.valueOf(elapsed))
.contentType(MediaType.APPLICATION_JSON)
.body(ApiResult.success(userDTO));3.3 Mockito 的 verify Builder
Mockito 框架中的 verify 也是 Builder 模式的应用:
// 验证某个方法被调用了特定次数
verify(mockService, times(2)).processOrder(any(Order.class));
verify(mockService, timeout(1000).atLeastOnce()).sendNotification(anyString());四、生产级代码实现
4.1 手写 Builder:HTTP 请求构建器
/**
* HTTP 请求配置类(不可变对象)
* 通过Builder构建,构建完成后不允许修改
*/
public final class HttpRequestConfig {
private final String url;
private final HttpMethod method;
private final Map<String, String> headers;
private final Map<String, String> queryParams;
private final Object requestBody;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final int maxRetries;
private final long retryIntervalMs;
private final boolean followRedirects;
private final String charset;
// 私有构造函数,只能通过Builder创建
private HttpRequestConfig(Builder builder) {
this.url = Objects.requireNonNull(builder.url, "URL is required");
this.method = Objects.requireNonNull(builder.method, "HTTP method is required");
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
this.requestBody = builder.requestBody;
this.connectTimeoutMs = builder.connectTimeoutMs;
this.readTimeoutMs = builder.readTimeoutMs;
this.maxRetries = builder.maxRetries;
this.retryIntervalMs = builder.retryIntervalMs;
this.followRedirects = builder.followRedirects;
this.charset = builder.charset;
}
// 只提供getter,没有setter,保证不可变性
public String getUrl() { return url; }
public HttpMethod getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getQueryParams() { return queryParams; }
public Object getRequestBody() { return requestBody; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public int getReadTimeoutMs() { return readTimeoutMs; }
public int getMaxRetries() { return maxRetries; }
public long getRetryIntervalMs() { return retryIntervalMs; }
public boolean isFollowRedirects() { return followRedirects; }
public String getCharset() { return charset; }
/**
* 静态内部Builder类
*/
public static class Builder {
// 必填字段
private final String url;
private final HttpMethod method;
// 可选字段,带默认值
private Map<String, String> headers = new HashMap<>();
private Map<String, String> queryParams = new HashMap<>();
private Object requestBody;
private int connectTimeoutMs = 5000; // 默认5秒连接超时
private int readTimeoutMs = 30000; // 默认30秒读取超时
private int maxRetries = 3; // 默认重试3次
private long retryIntervalMs = 1000; // 默认1秒重试间隔
private boolean followRedirects = true;
private String charset = "UTF-8";
// 必填参数通过构造函数传入
public Builder(String url, HttpMethod method) {
this.url = url;
this.method = method;
}
// 可选参数通过链式方法设置
public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}
public Builder headers(Map<String, String> headers) {
this.headers.putAll(headers);
return this;
}
public Builder queryParam(String name, String value) {
this.queryParams.put(name, value);
return this;
}
public Builder bearerToken(String token) {
return header("Authorization", "Bearer " + token);
}
public Builder contentTypeJson() {
return header("Content-Type", "application/json");
}
public Builder requestBody(Object body) {
this.requestBody = body;
return this;
}
public Builder connectTimeout(int ms) {
if (ms <= 0) throw new IllegalArgumentException("Connect timeout must be positive");
this.connectTimeoutMs = ms;
return this;
}
public Builder readTimeout(int ms) {
if (ms <= 0) throw new IllegalArgumentException("Read timeout must be positive");
this.readTimeoutMs = ms;
return this;
}
public Builder maxRetries(int retries) {
if (retries < 0) throw new IllegalArgumentException("Max retries must be >= 0");
this.maxRetries = retries;
return this;
}
public Builder retryInterval(long ms) {
this.retryIntervalMs = ms;
return this;
}
public Builder noRetry() {
return maxRetries(0);
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow;
return this;
}
public Builder charset(String charset) {
this.charset = charset;
return this;
}
/**
* 构建最终对象
*/
public HttpRequestConfig build() {
validate();
return new HttpRequestConfig(this);
}
private void validate() {
if (StringUtils.isBlank(url)) {
throw new IllegalStateException("URL cannot be blank");
}
if (method == null) {
throw new IllegalStateException("HTTP method cannot be null");
}
if (method == HttpMethod.GET && requestBody != null) {
log.warn("GET request should not have a body, body will be ignored");
}
}
}
// 便捷工厂方法,创建常用请求类型的Builder
public static Builder get(String url) {
return new Builder(url, HttpMethod.GET);
}
public static Builder post(String url) {
return new Builder(url, HttpMethod.POST).contentTypeJson();
}
public static Builder put(String url) {
return new Builder(url, HttpMethod.PUT).contentTypeJson();
}
public static Builder delete(String url) {
return new Builder(url, HttpMethod.DELETE);
}
}
// 使用示例
HttpRequestConfig config = HttpRequestConfig
.post("https://api.payment.com/v2/transactions")
.bearerToken(accessToken)
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.requestBody(transactionRequest)
.connectTimeout(3000)
.readTimeout(15000)
.maxRetries(3)
.retryInterval(500)
.build();4.2 带 Director 的经典建造者模式
/**
* 报表配置(复杂对象)
*/
public class ReportConfig {
private String reportName;
private LocalDate startDate;
private LocalDate endDate;
private List<String> dimensions;
private List<String> metrics;
private List<FilterCondition> filters;
private SortConfig sort;
private PaginationConfig pagination;
private ExportFormat exportFormat;
private boolean includeSubtotals;
private boolean includeTotals;
// getters...
public static class Builder {
private ReportConfig config = new ReportConfig();
public Builder reportName(String name) {
config.reportName = name;
return this;
}
public Builder dateRange(LocalDate start, LocalDate end) {
if (start.isAfter(end)) {
throw new IllegalArgumentException("Start date must be before end date");
}
config.startDate = start;
config.endDate = end;
return this;
}
public Builder lastNDays(int days) {
config.endDate = LocalDate.now();
config.startDate = LocalDate.now().minusDays(days);
return this;
}
public Builder dimensions(String... dimensions) {
config.dimensions = Arrays.asList(dimensions);
return this;
}
public Builder metrics(String... metrics) {
config.metrics = Arrays.asList(metrics);
return this;
}
public Builder filter(String field, String operator, Object value) {
if (config.filters == null) config.filters = new ArrayList<>();
config.filters.add(new FilterCondition(field, operator, value));
return this;
}
public Builder sortBy(String field, boolean ascending) {
config.sort = new SortConfig(field, ascending);
return this;
}
public Builder paginate(int pageNum, int pageSize) {
config.pagination = new PaginationConfig(pageNum, pageSize);
return this;
}
public Builder exportAs(ExportFormat format) {
config.exportFormat = format;
return this;
}
public Builder includeSubtotals() {
config.includeSubtotals = true;
return this;
}
public Builder includeTotals() {
config.includeTotals = true;
return this;
}
public ReportConfig build() {
Objects.requireNonNull(config.reportName, "Report name is required");
Objects.requireNonNull(config.startDate, "Start date is required");
Objects.requireNonNull(config.endDate, "End date is required");
if (config.metrics == null || config.metrics.isEmpty()) {
throw new IllegalStateException("At least one metric is required");
}
return config;
}
}
}
/**
* Director:封装常用报表配置的构建逻辑
*/
public class ReportConfigDirector {
/**
* 构建标准日销售报表
*/
public ReportConfig buildDailySalesReport(LocalDate date) {
return new ReportConfig.Builder()
.reportName("日销售报表_" + date)
.dateRange(date, date)
.dimensions("product_category", "region", "channel")
.metrics("order_count", "revenue", "avg_order_value", "refund_rate")
.sortBy("revenue", false) // 按收入降序
.includeSubtotals()
.includeTotals()
.exportAs(ExportFormat.EXCEL)
.build();
}
/**
* 构建月度用户留存分析报表
*/
public ReportConfig buildMonthlyRetentionReport(int year, int month) {
LocalDate startDate = LocalDate.of(year, month, 1);
LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth());
return new ReportConfig.Builder()
.reportName(String.format("用户留存报表_%d年%d月", year, month))
.dateRange(startDate, endDate)
.dimensions("user_cohort", "retention_day")
.metrics("user_count", "retention_rate", "revenue_per_user")
.filter("user_type", "eq", "registered")
.paginate(1, 100)
.exportAs(ExportFormat.CSV)
.build();
}
/**
* 构建实时监控报表(近30分钟)
*/
public ReportConfig buildRealtimeMonitorReport() {
return new ReportConfig.Builder()
.reportName("实时业务监控")
.lastNDays(0) // 今天
.dimensions("api_endpoint", "status_code")
.metrics("request_count", "error_rate", "p99_latency", "avg_latency")
.sortBy("error_rate", false)
.filter("latency", "gt", 1000) // 只看响应超过1秒的接口
.paginate(1, 50)
.build();
}
}4.3 Lombok @Builder 的正确使用与坑点规避
/**
* Lombok @Builder 的最佳实践
*/
@Builder
@Getter
@ToString
public class TradeOrder {
@NonNull
private String orderId;
@NonNull
private Long userId;
@NonNull
private BigDecimal amount;
private String currency;
private String channelCode;
// 坑1:@Builder默认不设置字段的默认值,下面这个在@Builder时会是null
// 解决方案:使用 @Builder.Default 注解
@Builder.Default
private String status = "PENDING";
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
@Builder.Default
private List<OrderItem> items = new ArrayList<>();
// 坑2:需要Jackson反序列化时,必须加 @JsonDeserialize(builder = ...)
// 或者使用 @Jacksonized 注解(Lombok 1.18.14+)
// @Jacksonized 必须与 @Builder 一起使用
}
/**
* @Builder 与继承的正确处理方式
* 坑:@Builder 不支持继承,子类Builder无法自动包含父类字段
*/
@Getter
public class BaseEntity {
private Long id;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long createdBy;
private Long updatedBy;
// 不要在父类上用@Builder,子类无法正确继承
}@Getter
@ToString(callSuper = true)
public class UserProfile extends BaseEntity {
private String username;
private String email;
private String phone;
private Integer age;
// 正确方式:在子类中手写Builder,调用super字段设置
@Builder(builderMethodName = "userProfileBuilder")
public UserProfile(Long id, LocalDateTime createdAt, LocalDateTime updatedAt,
Long createdBy, Long updatedBy,
String username, String email, String phone, Integer age) {
// 通过父类的setter设置父类字段(需要父类字段有setter)
// 或者父类提供protected构造函数
this.username = username;
this.email = email;
this.phone = phone;
this.age = age;
}
// 更好的方式:使用 @SuperBuilder(Lombok 1.18.2+)
}
/**
* @SuperBuilder 解决继承问题(推荐)
*/
@SuperBuilder
@Getter
public class BaseAuditEntity {
@Builder.Default
private Long id = 0L;
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
private Long createdBy;
}@SuperBuilder
@Getter
public class ProductSku extends BaseAuditEntity {
private String skuCode;
private String productId;
private BigDecimal price;
private Integer stock;
@Builder.Default
private Boolean enabled = true;
}
// 使用方式(子类Builder自动包含父类字段)
ProductSku sku = ProductSku.builder()
.id(1001L) // 父类字段
.createdBy(9L) // 父类字段
.skuCode("SKU-001") // 子类字段
.productId("PROD-A")
.price(new BigDecimal("99.99"))
.stock(100)
.build();五、Lombok @Builder 的陷阱深度分析
陷阱一:默认值被吞掉
// 错误写法:没有@Builder.Default,字段赋值被Builder忽略
@Builder
public class Config {
private int maxRetries = 3; // 这个默认值会被Builder忽略!
private String charset = "UTF-8"; // 同上
}
// Config.builder().build() 得到的是 maxRetries=0, charset=null !!!
// 因为Lombok生成的Builder中,所有字段初始值都是Java类型默认值
// 正确写法
@Builder
public class Config {
@Builder.Default
private int maxRetries = 3;
@Builder.Default
private String charset = "UTF-8";
}陷阱二:@Builder 与 @AllArgsConstructor 冲突
// 错误:同时使用@Builder和@AllArgsConstructor会导致编译失败
// 因为@Builder已经生成了全参构造函数
@Builder
@AllArgsConstructor // 冲突!
@NoArgsConstructor // 也需要加上,否则JSON反序列化失败
public class MyEntity {
private String name;
private int value;
}
// 正确方式:
@Builder
@NoArgsConstructor
@AllArgsConstructor // 此时手动声明,Builder会使用它
public class MyEntity {
private String name;
private int value;
}陷阱三:Jackson反序列化
// @Builder生成的类没有无参构造函数,Jackson无法反序列化
@Builder
public class OrderRequest {
private String orderId;
private BigDecimal amount;
}
// 调用 objectMapper.readValue(json, OrderRequest.class) 会报错
// 解决方案一:使用 @Jacksonized(推荐,Lombok 1.18.14+)
@Builder
@Jacksonized
public class OrderRequest {
private String orderId;
private BigDecimal amount;
}
// 解决方案二:手动添加 @JsonDeserialize
@Builder
@JsonDeserialize(builder = OrderRequest.OrderRequestBuilder.class)
public class OrderRequest {
private String orderId;
private BigDecimal amount;
@JsonPOJOBuilder(withPrefix = "")
public static class OrderRequestBuilder {
// Lombok自动生成,这里只需要标注注解
}
}陷阱四:@Builder 与 Spring Validation 的问题
// 使用@Builder后,Bean Validation注解可能不生效
@Builder
@Validated
public class CreateUserRequest {
@NotBlank
private String username;
@Email
private String email;
@Min(18)
private Integer age;
}
// 问题:Builder构建对象时不会触发Bean Validation
// 需要在build()之后手动调用验证,或者在Builder.build()方法中加入验证
// 解决方案:自定义build()方法
@Builder
public class CreateUserRequest {
@NotBlank
private String username;
@Email
private String email;
@Min(18)
private Integer age;
// 覆盖Lombok生成的Builder的build方法
public static class CreateUserRequestBuilder {
public CreateUserRequest build() {
CreateUserRequest request = new CreateUserRequest(username, email, age);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<CreateUserRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
String errors = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
throw new IllegalStateException("Validation failed: " + errors);
}
return request;
}
}
}六、踩坑实录
坑一:Builder 生成的对象不是不可变的
Lombok @Builder 生成的对象默认是可变的,因为字段没有 final。在高并发场景下,如果这个对象被共享,可能被其他线程修改,出现数据竞争。
正确做法:配合 @Value(Lombok 的不可变类注解)或者手动将字段声明为 final 来保证不可变性。但 @Value 不能与 @Builder 直接结合,需要用 @Value + @Builder 并手动处理。
坑二:大量 Optional 参数的 Builder 链过长
一个有30个字段的配置类,用 Builder 写出来的链式调用有几十行。每次看到这种代码就很痛苦,而且字段的层次结构也体现不出来。
解决方案:将配置按业务维度分组,拆成多个子配置类,每个子配置类各自有 Builder,然后在主配置类中聚合子配置:
// 分组配置,更清晰
HttpClientConfig config = HttpClientConfig.builder()
.connectionConfig(ConnectionConfig.builder()
.connectTimeout(5000)
.readTimeout(30000)
.maxConnections(200)
.build())
.retryConfig(RetryConfig.builder()
.maxRetries(3)
.retryInterval(1000)
.retryOn(IOException.class, HttpServerErrorException.class)
.build())
.authConfig(AuthConfig.bearer(token))
.build();坑三:copy-with-modification 场景的繁琐
Builder 模式创建对象后,想要"在已有对象基础上修改一两个字段"非常麻烦,需要把所有字段一一复制到新 Builder 中。
Lombok 提供了 toBuilder = true 属性来解决这个问题:
@Builder(toBuilder = true)
public class PaymentRequest {
private String orderId;
private BigDecimal amount;
private String currency;
private String callbackUrl;
}
// 使用 toBuilder() 方法,基于现有对象创建新的Builder
PaymentRequest original = PaymentRequest.builder()
.orderId("ORD-001")
.amount(new BigDecimal("100.00"))
.currency("CNY")
.callbackUrl("https://example.com/callback")
.build();
// 只修改amount,其他字段保持不变
PaymentRequest modified = original.toBuilder()
.amount(new BigDecimal("200.00"))
.build();七、总结
建造者模式在 Java 工程中是使用频率最高的模式之一,几乎每个稍微复杂的配置类都会用到它。
核心要点:
- 必填参数用构造函数,可选参数用Builder方法,这样能在编译期就保证必要字段不缺失。
- Lombok
@Builder用@Builder.Default声明默认值,否则默认值会被吞掉。 - 继承场景用
@SuperBuilder,不要在父类和子类上同时用@Builder。 - Jackson 反序列化加
@Jacksonized,避免运行时反序列化失败。 - 考虑不可变性:Builder 构建出来的对象,字段尽量是
final的,避免并发问题。 toBuilder = true是应对"复制修改"场景的神器,要善用。
