设计模式:建造者模式

欢迎你来读这篇博客,这篇博客主要是关于建造者模式
其中包括建造者模式的核心思想、适用场景、优缺点、传统写法、链式写法、Lombok 写法,以及 Java 后端开发中的实际案例。

序言

在 Java 开发中,我们经常会遇到一种对象:字段很多,构建过程复杂,而且有些字段是必填的,有些字段是可选的。

比如一个订单查询条件对象:

  • 用户 ID;
  • 订单状态;
  • 开始时间;
  • 结束时间;
  • 页码;
  • 每页条数;
  • 排序字段;
  • 是否包含已删除数据;
  • 是否只查询异常订单;
  • 是否需要关联支付信息。

如果直接写构造方法,很容易变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
OrderQuery query = new OrderQuery(
1001L,
"PAID",
startTime,
endTime,
1,
20,
"create_time",
false,
false,
true
);

这段代码的问题很明显:

调用者根本看不出来每个参数是什么意思。

尤其是连续出现多个 BooleanIntegerString 时,代码可读性会非常差。

更危险的是,参数顺序一旦传错,编译器可能还不会报错。

比如:

1
new OrderQuery(1001L, "PAID", startTime, endTime, 20, 1, "create_time", false, false, true);

pageNopageSize 传反了,编译能过,运行结果错。这个 bug 非常安静,安静得像在代码里潜伏的刺客。

建造者模式就是为了解决这类问题而出现的。

它的核心目标是:

将复杂对象的构建过程与对象本身分离,让对象创建过程更清晰、更安全、更易读。

正文

chapter 1:什么是建造者模式

建造者模式,英文是 Builder Pattern,属于创建型设计模式。

它的定义是:

将一个复杂对象的构建过程与它的表示分离,使同样的构建过程可以创建不同的表示。

简单理解:

建造者模式不是一次性通过构造方法塞入所有参数,而是一步一步设置对象属性,最后统一构建出目标对象。

它特别适合用来创建字段较多、构建过程复杂、可选参数很多的对象。

常见写法如下:

1
2
3
4
5
6
7
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(20)
.includePayment(true)
.build();

和长构造方法相比,这种写法有几个好处:

  • 参数含义清晰;
  • 可选参数可以按需设置;
  • 不容易传错顺序;
  • 代码可读性更高;
  • 可以在 build() 方法中统一校验参数。

chapter 2:为什么需要建造者模式

对象创建常见有三种方式:

  1. 构造方法;
  2. JavaBean setter;
  3. Builder 建造者。

1. 构造方法的问题

当字段很少时,构造方法很好用。

1
2
3
4
public User(Long id, String username) {
this.id = id;
this.username = username;
}

但是字段多了之后,构造方法会失控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public OrderQuery(Long userId,
String status,
LocalDateTime startTime,
LocalDateTime endTime,
Integer pageNo,
Integer pageSize,
String sortField,
Boolean includeDeleted,
Boolean onlyException,
Boolean includePayment) {
this.userId = userId;
this.status = status;
this.startTime = startTime;
this.endTime = endTime;
this.pageNo = pageNo;
this.pageSize = pageSize;
this.sortField = sortField;
this.includeDeleted = includeDeleted;
this.onlyException = onlyException;
this.includePayment = includePayment;
}

调用时代码就会变得非常难读。

1
2
3
4
5
6
7
8
9
10
11
12
OrderQuery query = new OrderQuery(
1001L,
"PAID",
startTime,
endTime,
1,
20,
"create_time",
false,
false,
true
);

这就是典型的“望参兴叹”。

2. JavaBean setter 的问题

使用 setter 可以提高可读性。

1
2
3
4
5
6
OrderQuery query = new OrderQuery();
query.setUserId(1001L);
query.setStatus("PAID");
query.setPageNo(1);
query.setPageSize(20);
query.setIncludePayment(true);

这种写法的问题是:

  • 对象创建过程不够完整;
  • 对象可能处于中间状态;
  • 字段可以被随意修改;
  • 不利于不可变对象设计;
  • 参数校验容易分散在各个 setter 中。

例如:

1
2
3
4
OrderQuery query = new OrderQuery();
query.setUserId(1001L);

// 这里对象已经被创建出来,但还没有设置分页参数

如果对象在构建过程中被传递给其他线程或方法,就可能出现状态不完整的问题。

3. Builder 的优势

Builder 的写法是:

1
2
3
4
5
6
7
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(20)
.includePayment(true)
.build();

它兼顾了构造方法和 setter 的优点:

  • 像 setter 一样可读;
  • 像构造方法一样可以一次性构建完整对象;
  • 可以在 build() 中统一校验;
  • 可以创建不可变对象;
  • 可以避免构造方法参数爆炸。

chapter 3:建造者模式的经典角色

传统建造者模式一般包括以下角色:

  1. Product 产品类:最终要创建的复杂对象。
  2. Builder 抽象建造者:定义创建产品各个部分的接口。
  3. ConcreteBuilder 具体建造者:实现具体构建步骤。
  4. Director 指挥者:控制构建流程。
  5. Client 客户端:使用 Director 和 Builder 构建对象。

经典结构如下:

1
2
3
4
5
6
7
8
9
10
Client


Director ───────► Builder


ConcreteBuilder


Product

不过在现代 Java 开发中,最常用的是简化版 Builder:

1
2
Product
└── static class Builder

也就是把 Builder 写成产品类的静态内部类。

chapter 4:传统建造者模式案例:构建报表导出任务

先看一个传统版案例。

假设我们要构建一个报表导出任务 ReportExportTask

一个导出任务包含:

  • 报表名称;
  • 导出格式;
  • 查询条件;
  • 文件名;
  • 是否压缩;
  • 是否上传到对象存储;
  • 回调地址。

1. 产品类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ReportExportTask {

private String reportName;
private String format;
private String queryCondition;
private String filename;
private boolean compressed;
private boolean uploadToStorage;
private String callbackUrl;

public void setReportName(String reportName) {
this.reportName = reportName;
}

public void setFormat(String format) {
this.format = format;
}

public void setQueryCondition(String queryCondition) {
this.queryCondition = queryCondition;
}

public void setFilename(String filename) {
this.filename = filename;
}

public void setCompressed(boolean compressed) {
this.compressed = compressed;
}

public void setUploadToStorage(boolean uploadToStorage) {
this.uploadToStorage = uploadToStorage;
}

public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
}

@Override
public String toString() {
return "ReportExportTask{" +
"reportName='" + reportName + '\'' +
", format='" + format + '\'' +
", queryCondition='" + queryCondition + '\'' +
", filename='" + filename + '\'' +
", compressed=" + compressed +
", uploadToStorage=" + uploadToStorage +
", callbackUrl='" + callbackUrl + '\'' +
'}';
}
}

2. 抽象建造者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface ReportExportTaskBuilder {

void buildReportName();

void buildFormat();

void buildQueryCondition();

void buildFilename();

void buildCompressed();

void buildUploadToStorage();

void buildCallbackUrl();

ReportExportTask getResult();
}

3. 具体建造者:Excel 报表任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ExcelReportExportTaskBuilder implements ReportExportTaskBuilder {

private final ReportExportTask task = new ReportExportTask();

@Override
public void buildReportName() {
task.setReportName("订单报表");
}

@Override
public void buildFormat() {
task.setFormat("xlsx");
}

@Override
public void buildQueryCondition() {
task.setQueryCondition("status = PAID");
}

@Override
public void buildFilename() {
task.setFilename("order-report.xlsx");
}

@Override
public void buildCompressed() {
task.setCompressed(false);
}

@Override
public void buildUploadToStorage() {
task.setUploadToStorage(true);
}

@Override
public void buildCallbackUrl() {
task.setCallbackUrl("https://example.com/callback/report");
}

@Override
public ReportExportTask getResult() {
return task;
}
}

4. 指挥者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ReportExportTaskDirector {

private final ReportExportTaskBuilder builder;

public ReportExportTaskDirector(ReportExportTaskBuilder builder) {
this.builder = builder;
}

public ReportExportTask construct() {
builder.buildReportName();
builder.buildFormat();
builder.buildQueryCondition();
builder.buildFilename();
builder.buildCompressed();
builder.buildUploadToStorage();
builder.buildCallbackUrl();

return builder.getResult();
}
}

5. 客户端

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {

public static void main(String[] args) {
ReportExportTaskBuilder builder = new ExcelReportExportTaskBuilder();

ReportExportTaskDirector director = new ReportExportTaskDirector(builder);

ReportExportTask task = director.construct();

System.out.println(task);
}
}

这种传统写法适合构建流程比较固定、构建步骤比较多、不同建造者生成不同产品表现形式的场景。

但是在普通 Java 后端开发中,这种写法有点重。

实际项目中更常用的是链式 Builder。

chapter 5:链式 Builder 写法

链式 Builder 是现代 Java 中最常见的写法。

它通常把 Builder 作为目标类的静态内部类。

下面用订单查询条件作为案例。

1. 产品类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import java.time.LocalDateTime;

public class OrderQuery {

private final Long userId;
private final String status;
private final LocalDateTime startTime;
private final LocalDateTime endTime;
private final Integer pageNo;
private final Integer pageSize;
private final String sortField;
private final Boolean includeDeleted;
private final Boolean onlyException;
private final Boolean includePayment;

private OrderQuery(Builder builder) {
this.userId = builder.userId;
this.status = builder.status;
this.startTime = builder.startTime;
this.endTime = builder.endTime;
this.pageNo = builder.pageNo;
this.pageSize = builder.pageSize;
this.sortField = builder.sortField;
this.includeDeleted = builder.includeDeleted;
this.onlyException = builder.onlyException;
this.includePayment = builder.includePayment;
}

public static Builder builder() {
return new Builder();
}

public Long getUserId() {
return userId;
}

public String getStatus() {
return status;
}

public LocalDateTime getStartTime() {
return startTime;
}

public LocalDateTime getEndTime() {
return endTime;
}

public Integer getPageNo() {
return pageNo;
}

public Integer getPageSize() {
return pageSize;
}

public String getSortField() {
return sortField;
}

public Boolean getIncludeDeleted() {
return includeDeleted;
}

public Boolean getOnlyException() {
return onlyException;
}

public Boolean getIncludePayment() {
return includePayment;
}

public static class Builder {

private Long userId;
private String status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer pageNo = 1;
private Integer pageSize = 20;
private String sortField = "create_time";
private Boolean includeDeleted = false;
private Boolean onlyException = false;
private Boolean includePayment = false;

private Builder() {
}

public Builder userId(Long userId) {
this.userId = userId;
return this;
}

public Builder status(String status) {
this.status = status;
return this;
}

public Builder startTime(LocalDateTime startTime) {
this.startTime = startTime;
return this;
}

public Builder endTime(LocalDateTime endTime) {
this.endTime = endTime;
return this;
}

public Builder pageNo(Integer pageNo) {
this.pageNo = pageNo;
return this;
}

public Builder pageSize(Integer pageSize) {
this.pageSize = pageSize;
return this;
}

public Builder sortField(String sortField) {
this.sortField = sortField;
return this;
}

public Builder includeDeleted(Boolean includeDeleted) {
this.includeDeleted = includeDeleted;
return this;
}

public Builder onlyException(Boolean onlyException) {
this.onlyException = onlyException;
return this;
}

public Builder includePayment(Boolean includePayment) {
this.includePayment = includePayment;
return this;
}

public OrderQuery build() {
validate();
return new OrderQuery(this);
}

private void validate() {
if (pageNo == null || pageNo <= 0) {
throw new IllegalArgumentException("pageNo must be greater than 0");
}

if (pageSize == null || pageSize <= 0) {
throw new IllegalArgumentException("pageSize must be greater than 0");
}

if (startTime != null && endTime != null && startTime.isAfter(endTime)) {
throw new IllegalArgumentException("startTime must be before endTime");
}
}
}
}

2. 使用方式

1
2
3
4
5
6
7
8
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(50)
.sortField("pay_time")
.includePayment(true)
.build();

这段代码的可读性比长构造方法强很多。

调用者一眼就能看出每个参数的含义。

3. 默认值处理

Builder 里可以设置默认值:

1
2
3
4
private Integer pageNo = 1;
private Integer pageSize = 20;
private String sortField = "create_time";
private Boolean includeDeleted = false;

这样调用者不传时,也有合理默认值。

4. 参数校验

可以在 build() 方法中集中校验:

1
2
3
4
public OrderQuery build() {
validate();
return new OrderQuery(this);
}

这样可以保证最终创建出来的对象是合法的。

chapter 6:为什么 Builder 适合不可变对象

建造者模式很适合构建不可变对象。

不可变对象的特点是:

  • 字段使用 final
  • 不提供 setter;
  • 对象创建完成后状态不可修改;
  • 天然更适合并发场景。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class UserProfile {

private final Long userId;
private final String nickname;
private final String avatarUrl;
private final String email;
private final String phone;

private UserProfile(Builder builder) {
this.userId = builder.userId;
this.nickname = builder.nickname;
this.avatarUrl = builder.avatarUrl;
this.email = builder.email;
this.phone = builder.phone;
}

public static Builder builder(Long userId, String nickname) {
return new Builder(userId, nickname);
}

public static class Builder {

private final Long userId;
private final String nickname;

private String avatarUrl;
private String email;
private String phone;

private Builder(Long userId, String nickname) {
if (userId == null) {
throw new IllegalArgumentException("userId can not be null");
}

if (nickname == null || nickname.isBlank()) {
throw new IllegalArgumentException("nickname can not be blank");
}

this.userId = userId;
this.nickname = nickname;
}

public Builder avatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
return this;
}

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

public Builder phone(String phone) {
this.phone = phone;
return this;
}

public UserProfile build() {
return new UserProfile(this);
}
}
}

调用方式:

1
2
3
4
UserProfile profile = UserProfile.builder(1001L, "SuperMario")
.avatarUrl("https://example.com/avatar.png")
.email("user@example.com")
.build();

这里 userIdnickname 是必填字段,其他字段是可选字段。

这种写法非常适合领域对象、值对象、配置对象、请求参数对象。

chapter 7:建造者模式和构造方法重载的区别

构造方法重载常见写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {

public User(Long id) {
}

public User(Long id, String username) {
}

public User(Long id, String username, String email) {
}

public User(Long id, String username, String email, String phone) {
}
}

这叫“伸缩构造器模式”。

字段少时还能接受,字段多时很难维护。

Builder 写法则是:

1
2
3
4
5
6
User user = User.builder()
.id(1001L)
.username("mario")
.email("mario@example.com")
.phone("13800000000")
.build();

对比一下:

对比项 构造方法重载 建造者模式
参数可读性
可选参数支持
参数顺序错误 容易发生 不容易发生
对象校验 分散 可集中在 build()
不可变对象支持 支持,但构造器会很长 非常适合
代码复杂度 字段少时低 字段多时更清晰

chapter 8:建造者模式和 JavaBean setter 的区别

JavaBean setter 写法:

1
2
3
4
5
OrderQuery query = new OrderQuery();
query.setUserId(1001L);
query.setStatus("PAID");
query.setPageNo(1);
query.setPageSize(20);

Builder 写法:

1
2
3
4
5
6
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(20)
.build();

主要区别如下:

对比项 JavaBean setter Builder
可读性 较好 很好
是否存在中间状态 存在 较少
是否适合不可变对象 不适合 适合
参数校验 容易分散 可集中
线程安全 可变对象风险较高 不可变对象更友好
使用成本 中等

setter 更适合普通 DTO、Entity、框架反序列化对象。

Builder 更适合复杂请求对象、配置对象、领域值对象。

chapter 9:Lombok 的 @Builder

在真实 Java 项目中,很多人不会手写 Builder,而是使用 Lombok 的 @Builder

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class OrderQuery {

private Long userId;

private String status;

private LocalDateTime startTime;

private LocalDateTime endTime;

@Builder.Default
private Integer pageNo = 1;

@Builder.Default
private Integer pageSize = 20;

@Builder.Default
private String sortField = "create_time";

@Builder.Default
private Boolean includeDeleted = false;

@Builder.Default
private Boolean includePayment = false;
}

使用方式:

1
2
3
4
5
6
7
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(20)
.includePayment(true)
.build();

使用 @Builder.Default 的原因

如果字段有默认值,Lombok Builder 需要加 @Builder.Default

否则:

1
private Integer pageNo = 1;

这个默认值在 Builder 构建时可能不会生效。

正确写法是:

1
2
@Builder.Default
private Integer pageNo = 1;

Lombok @Builder 的优点

  • 减少样板代码;
  • 写法简洁;
  • 可读性好;
  • 适合 DTO、VO、Command、Query 对象。

Lombok @Builder 的注意点

  1. 默认值要使用 @Builder.Default
  2. 如果需要参数校验,不能只依赖 Lombok,需要额外处理。
  3. 对 JPA Entity 慎用 @Builder
  4. 团队需要统一 Lombok 使用规范。
  5. 如果字段很多,Builder 也可能被滥用。

chapter 10:Builder 中如何做参数校验

如果手写 Builder,可以在 build() 中做校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public OrderQuery build() {
validate();
return new OrderQuery(this);
}

private void validate() {
if (pageNo == null || pageNo <= 0) {
throw new IllegalArgumentException("pageNo must be greater than 0");
}

if (pageSize == null || pageSize <= 0) {
throw new IllegalArgumentException("pageSize must be greater than 0");
}

if (startTime != null && endTime != null && startTime.isAfter(endTime)) {
throw new IllegalArgumentException("startTime must be before endTime");
}
}

如果使用 Lombok,可以考虑在构造方法中校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.Builder;
import lombok.Getter;

@Getter
public class PageQuery {

private final Integer pageNo;

private final Integer pageSize;

@Builder
public PageQuery(Integer pageNo, Integer pageSize) {
if (pageNo == null || pageNo <= 0) {
throw new IllegalArgumentException("pageNo must be greater than 0");
}

if (pageSize == null || pageSize <= 0) {
throw new IllegalArgumentException("pageSize must be greater than 0");
}

this.pageNo = pageNo;
this.pageSize = pageSize;
}
}

调用方式:

1
2
3
4
PageQuery query = PageQuery.builder()
.pageNo(1)
.pageSize(20)
.build();

这样既能使用 Lombok Builder,又能保证参数合法性。

chapter 11:案例:构建 HTTP 请求对象

建造者模式在 Java 后端里非常适合构建 HTTP 请求对象。

假设我们封装一个三方 API 请求。

请求对象包括:

  • URL;
  • HTTP 方法;
  • Header;
  • Query 参数;
  • Body;
  • 超时时间;
  • 是否重试;
  • 重试次数。

直接构造会很难看。

使用 Builder 会清晰很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ApiRequest {

private final String url;
private final String method;
private final Map<String, String> headers;
private final Map<String, String> queryParams;
private final String body;
private final Duration timeout;
private final boolean retryEnabled;
private final int maxRetryTimes;

private ApiRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
this.body = builder.body;
this.timeout = builder.timeout;
this.retryEnabled = builder.retryEnabled;
this.maxRetryTimes = builder.maxRetryTimes;
}

public static Builder builder(String url, String method) {
return new Builder(url, method);
}

public String getUrl() {
return url;
}

public String getMethod() {
return method;
}

public Map<String, String> getHeaders() {
return headers;
}

public Map<String, String> getQueryParams() {
return queryParams;
}

public String getBody() {
return body;
}

public Duration getTimeout() {
return timeout;
}

public boolean isRetryEnabled() {
return retryEnabled;
}

public int getMaxRetryTimes() {
return maxRetryTimes;
}

public static class Builder {

private final String url;
private final String method;

private final Map<String, String> headers = new HashMap<>();
private final Map<String, String> queryParams = new HashMap<>();
private String body;
private Duration timeout = Duration.ofSeconds(3);
private boolean retryEnabled = false;
private int maxRetryTimes = 0;

private Builder(String url, String method) {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("url can not be blank");
}

if (method == null || method.isBlank()) {
throw new IllegalArgumentException("method can not be blank");
}

this.url = url;
this.method = method;
}

public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}

public Builder queryParam(String name, String value) {
this.queryParams.put(name, value);
return this;
}

public Builder body(String body) {
this.body = body;
return this;
}

public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}

public Builder enableRetry(int maxRetryTimes) {
this.retryEnabled = true;
this.maxRetryTimes = maxRetryTimes;
return this;
}

public ApiRequest build() {
validate();
return new ApiRequest(this);
}

private void validate() {
if (timeout == null || timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("timeout must be positive");
}

if (retryEnabled && maxRetryTimes <= 0) {
throw new IllegalArgumentException("maxRetryTimes must be greater than 0 when retry is enabled");
}
}
}
}

使用方式:

1
2
3
4
5
6
7
8
ApiRequest request = ApiRequest.builder("https://api.example.com/orders", "POST")
.header("Authorization", "Bearer token")
.header("Content-Type", "application/json")
.queryParam("source", "backend")
.body("{\"orderId\":1001}")
.timeout(Duration.ofSeconds(5))
.enableRetry(3)
.build();

这段代码读起来很清晰:

  • 请求哪个 URL;
  • 使用什么方法;
  • 设置了哪些 Header;
  • 设置了哪些 Query 参数;
  • Body 是什么;
  • 超时时间是多少;
  • 是否开启重试。

这就是 Builder 的价值:让复杂对象的构建过程具有表达力。

chapter 12:案例:构建领域事件对象

在 DDD 或事件驱动系统里,经常需要构建领域事件。

例如订单支付成功事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.time.LocalDateTime;
import java.util.Map;

public class OrderPaidEvent {

private final Long orderId;
private final Long userId;
private final Long paymentId;
private final Integer amount;
private final String currency;
private final LocalDateTime paidTime;
private final Map<String, String> attributes;

private OrderPaidEvent(Builder builder) {
this.orderId = builder.orderId;
this.userId = builder.userId;
this.paymentId = builder.paymentId;
this.amount = builder.amount;
this.currency = builder.currency;
this.paidTime = builder.paidTime;
this.attributes = builder.attributes;
}

public static Builder builder(Long orderId, Long userId) {
return new Builder(orderId, userId);
}

public static class Builder {

private final Long orderId;
private final Long userId;

private Long paymentId;
private Integer amount;
private String currency = "CNY";
private LocalDateTime paidTime = LocalDateTime.now();
private Map<String, String> attributes = Map.of();

private Builder(Long orderId, Long userId) {
if (orderId == null) {
throw new IllegalArgumentException("orderId can not be null");
}

if (userId == null) {
throw new IllegalArgumentException("userId can not be null");
}

this.orderId = orderId;
this.userId = userId;
}

public Builder paymentId(Long paymentId) {
this.paymentId = paymentId;
return this;
}

public Builder amount(Integer amount) {
this.amount = amount;
return this;
}

public Builder currency(String currency) {
this.currency = currency;
return this;
}

public Builder paidTime(LocalDateTime paidTime) {
this.paidTime = paidTime;
return this;
}

public Builder attributes(Map<String, String> attributes) {
this.attributes = attributes;
return this;
}

public OrderPaidEvent build() {
if (paymentId == null) {
throw new IllegalArgumentException("paymentId can not be null");
}

if (amount == null || amount <= 0) {
throw new IllegalArgumentException("amount must be greater than 0");
}

return new OrderPaidEvent(this);
}
}
}

调用方式:

1
2
3
4
5
6
OrderPaidEvent event = OrderPaidEvent.builder(10001L, 20001L)
.paymentId(30001L)
.amount(9900)
.currency("CNY")
.attributes(Map.of("channel", "ALI_PAY"))
.build();

领域事件通常字段不少,而且很多字段有业务约束。

使用 Builder 可以让事件构建更加清晰,也更容易做参数校验。

chapter 13:建造者模式在 JDK 和框架中的影子

建造者模式在 Java 生态中非常常见。

1. StringBuilder

虽然 StringBuilder 不是严格意义上的 GoF Builder,但它体现了“逐步构建复杂对象”的思想。

1
2
3
4
5
6
String content = new StringBuilder()
.append("Hello")
.append(", ")
.append("Builder")
.append("!")
.toString();

2. Spring Security

Spring Security 的配置大量使用链式构建风格。

1
2
3
4
5
6
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form.disable());

这是一种典型的 Fluent API 风格,和 Builder 思想很接近。

3. WebClient

Spring WebFlux 的 WebClient 也大量使用 Builder。

1
2
3
4
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Content-Type", "application/json")
.build();

4. OkHttp Request

OkHttp 的请求对象也是典型 Builder 风格。

1
2
3
4
Request request = new Request.Builder()
.url("https://api.example.com")
.header("Authorization", "Bearer token")
.build();

5. Lombok @Builder

Lombok 的 @Builder 是业务开发中最常见的 Builder 代码生成工具。

它能显著减少样板代码,但要注意默认值和参数校验问题。

chapter 14:建造者模式的优点

1. 提高代码可读性

链式调用能让参数含义非常清晰。

1
2
3
4
5
OrderQuery.builder()
.userId(1001L)
.status("PAID")
.includePayment(true)
.build();

比下面这种构造方法更容易理解:

1
new OrderQuery(1001L, "PAID", null, null, 1, 20, false, true);

2. 适合可选参数很多的对象

不需要为了不同参数组合写大量重载构造方法。

3. 可以集中校验参数

build() 方法中统一校验对象合法性。

4. 适合构建不可变对象

字段可以声明为 final,不提供 setter。

5. 构建过程更灵活

可以按需设置不同参数,不需要一次性传入所有字段。

chapter 15:建造者模式的缺点

1. 增加代码量

如果手写 Builder,会增加不少样板代码。

尤其是字段很多时,Builder 本身也会变长。

2. 简单对象不值得使用

如果一个类只有两三个字段,用构造方法就够了。

强行 Builder 只会让代码变啰嗦。

3. Builder 可能绕过业务约束

如果没有在 build() 中做校验,调用者可能构建出非法对象。

4. Lombok 可能隐藏细节

@Builder 很方便,但也容易让开发者忽略默认值、校验、JPA 兼容性等问题。

chapter 16:适用场景

建造者模式适合以下场景。

1. 对象字段很多

尤其是超过 4 个字段之后,就可以考虑 Builder。

当然,这不是硬性规定,而是可读性判断。

2. 可选参数很多

例如查询条件、配置对象、请求对象、事件对象。

3. 构建过程需要校验

可以在 build() 中集中校验。

4. 希望对象不可变

Builder 非常适合配合 final 字段使用。

5. 构建过程有默认值

Builder 可以很自然地设置默认值。

例如:

1
2
private Integer pageNo = 1;
private Integer pageSize = 20;

6. 构建过程需要表达语义

例如 HTTP 请求、SQL 查询条件、导出任务、消息发送任务。

chapter 17:不适合使用的场景

以下场景不建议使用 Builder。

1. 对象很简单

例如:

1
new Point(1, 2)

没必要写成:

1
2
3
4
Point.builder()
.x(1)
.y(2)
.build();

这不是优雅,这是“代码穿西装下地干活”。

2. 对象必须频繁修改

Builder 更适合构建对象,而不是频繁修改对象。

如果对象本身就是可变状态模型,setter 可能更合适。

3. 框架要求无参构造和 setter

例如一些序列化框架、ORM 框架可能更喜欢 JavaBean 风格。

JPA Entity 通常不建议过度使用 Builder,尤其是复杂关联关系场景。

4. 团队代码风格不统一

如果有人手写 Builder,有人用 Lombok,有人用 setter,风格会很乱。

最好在团队规范中明确哪些对象适合使用 Builder。

chapter 18:建造者模式和工厂模式的区别

建造者模式和工厂模式都属于创建型模式,但关注点不同。

对比项 工厂模式 建造者模式
关注点 创建哪一种对象 如何一步步构建复杂对象
适合对象 多态对象、不同实现类 字段复杂、参数很多的对象
典型问题 根据类型创建对象 构造参数过多、可选参数多
返回结果 通常直接返回产品 经过多个步骤后 build
示例 创建不同支付渠道 构建复杂查询条件

简单说:

工厂模式解决“创建哪个对象”。

建造者模式解决“怎么构建这个对象”。

例如:

1
Payment payment = PaymentFactory.create("ALI_PAY");

这是工厂模式。

1
2
3
4
5
6
OrderQuery query = OrderQuery.builder()
.userId(1001L)
.status("PAID")
.pageNo(1)
.pageSize(20)
.build();

这是建造者模式。

chapter 19:真实项目中的实践建议

1. DTO、VO、Query、Command 可以考虑 Builder

例如:

  • OrderQuery
  • CreateOrderCommand
  • PaymentNotifyDTO
  • UserProfileVO
  • ReportExportRequest

这些对象字段较多,用 Builder 很自然。

2. Entity 慎用 Builder

JPA Entity 通常需要无参构造方法,且生命周期由 ORM 管理。

如果对 Entity 使用 Builder,要注意:

  • 是否影响 JPA 代理;
  • 是否绕过领域方法;
  • 是否导致对象状态不完整;
  • 是否破坏聚合不变量。

更推荐在领域模型中使用有语义的静态工厂方法或领域方法。

例如:

1
Order order = Order.create(userId, items);

而不是:

1
2
3
4
5
Order.builder()
.userId(userId)
.items(items)
.status("PAID")
.build();

领域对象不是“字段袋子”,不要把所有字段都暴露给 Builder。

3. Builder 中保留业务校验

不要只为了链式调用而使用 Builder。

真正有价值的 Builder 应该能保证构建出来的对象是合法的。

1
2
3
4
public OrderQuery build() {
validate();
return new OrderQuery(this);
}

4. 使用 Lombok 时注意默认值

字段默认值必须使用:

1
2
@Builder.Default
private Integer pageSize = 20;

否则容易出现默认值不生效的问题。

5. 必填字段可以放在 builder 方法或 Builder 构造方法中

例如:

1
2
3
public static Builder builder(Long orderId, Long userId) {
return new Builder(orderId, userId);
}

这样可以从 API 设计层面告诉调用者:这些字段必须传。

6. 不要让 Builder 变成万能入口

如果一个对象有严格业务状态流转,不要让 Builder 暴露所有字段。

例如订单对象的状态应该由业务方法控制,而不是让调用者随便 .status("FINISHED")

这就像不能给用户一个后台数据库账号,然后说“你自己维护业务一致性”。人性经不起考验,代码也一样。

chapter 20:完整案例代码汇总

下面给出一个推荐的手写 Builder 完整示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ApiRequest {

private final String url;
private final String method;
private final Map<String, String> headers;
private final Map<String, String> queryParams;
private final String body;
private final Duration timeout;
private final boolean retryEnabled;
private final int maxRetryTimes;

private ApiRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
this.body = builder.body;
this.timeout = builder.timeout;
this.retryEnabled = builder.retryEnabled;
this.maxRetryTimes = builder.maxRetryTimes;
}

public static Builder builder(String url, String method) {
return new Builder(url, method);
}

public static class Builder {

private final String url;
private final String method;

private final Map<String, String> headers = new HashMap<>();
private final Map<String, String> queryParams = new HashMap<>();
private String body;
private Duration timeout = Duration.ofSeconds(3);
private boolean retryEnabled = false;
private int maxRetryTimes = 0;

private Builder(String url, String method) {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("url can not be blank");
}

if (method == null || method.isBlank()) {
throw new IllegalArgumentException("method can not be blank");
}

this.url = url;
this.method = method;
}

public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}

public Builder queryParam(String name, String value) {
this.queryParams.put(name, value);
return this;
}

public Builder body(String body) {
this.body = body;
return this;
}

public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}

public Builder enableRetry(int maxRetryTimes) {
this.retryEnabled = true;
this.maxRetryTimes = maxRetryTimes;
return this;
}

public ApiRequest build() {
validate();
return new ApiRequest(this);
}

private void validate() {
if (timeout == null || timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("timeout must be positive");
}

if (retryEnabled && maxRetryTimes <= 0) {
throw new IllegalArgumentException("maxRetryTimes must be greater than 0 when retry is enabled");
}
}
}
}

使用方式:

1
2
3
4
5
6
7
8
ApiRequest request = ApiRequest.builder("https://api.example.com/orders", "POST")
.header("Authorization", "Bearer token")
.header("Content-Type", "application/json")
.queryParam("source", "backend")
.body("{\"orderId\":1001}")
.timeout(Duration.ofSeconds(5))
.enableRetry(3)
.build();

chapter 21:一句话总结

建造者模式的本质是:

把复杂对象的构建过程封装起来,让调用者通过清晰的步骤构建对象,而不是面对一长串难以理解的构造参数。

它适合字段多、可选参数多、构建过程复杂、需要默认值和参数校验的对象。

在 Java 后端开发中,Builder 最常见的落地场景是:

  • 查询条件对象;
  • 请求参数对象;
  • 配置对象;
  • 领域事件;
  • API 请求对象;
  • 复杂 DTO / VO / Command。

如果对象很简单,就不用硬上 Builder。

真正好的设计不是“每个类都有 Builder”,而是“复杂对象值得 Builder,简单对象保持简单”。

参考资料

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
  • Joshua Bloch. Effective Java.
  • Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
  • Spring Framework Documentation: WebClient.
  • Project Lombok Documentation: @Builder.
  • Refactoring Guru: Builder Pattern.
  • SourceMaking: Builder Design Pattern.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:建造者模式
https://allendericdalexander.github.io/2026/04/01/java/design/05builder-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议