设计模式:原型模式

欢迎你来读这篇博客,这篇博客主要是关于原型模式
其中包括原型模式的核心思想、适用场景、浅拷贝与深拷贝、Cloneable 的问题、序列化拷贝、Spring 原型 Bean,以及 Java 后端开发中的真实案例。

序言

前面已经聊过简单工厂、工厂方法、抽象工厂、单例和建造者模式。

这些模式都在解决一个问题:

对象应该如何创建?

但是原型模式的思路有点不一样。

它不是从零开始创建对象,而是:

通过复制一个已有对象,快速创建一个新对象。

也就是说,原型模式关注的不是 new,而是 copy

在真实项目中,有些对象创建过程非常复杂:

  • 初始化字段很多;
  • 需要读取配置;
  • 需要计算默认值;
  • 需要组装规则;
  • 需要加载模板;
  • 需要进行较重的初始化逻辑。

如果每次都重新创建,会很麻烦,也可能影响性能。

这时就可以先准备一个“原型对象”,后续需要新对象时,直接基于原型复制一份,再修改少量差异字段。

这就像写文章时不会每次都从空白页开始,而是复制一个模板再改。程序员懒得很高级时,就叫设计模式。

正文

chapter 1:什么是原型模式

原型模式,英文是 Prototype Pattern,属于创建型设计模式。

它的定义是:

使用原型实例指定要创建对象的种类,并通过复制这个原型来创建新的对象。

简单说:

原型模式就是通过复制已有对象来创建新对象。

原型模式一般包含三个角色:

  1. Prototype 抽象原型:定义克隆方法。
  2. ConcretePrototype 具体原型:实现克隆方法。
  3. Client 客户端:通过克隆原型对象创建新对象。

典型结构如下:

1
2
3
4
5
6
7
8
Client


Prototype
│ clone()


ConcretePrototype

在 Java 中,原型模式经常会和 Cloneableclone()、拷贝构造方法、序列化、JSON 拷贝等方式联系在一起。

chapter 2:为什么需要原型模式

假设系统中有一个复杂的报表配置对象。

它包含:

  • 报表名称;
  • 数据源配置;
  • 查询条件;
  • 字段列表;
  • 排序规则;
  • 权限规则;
  • 导出格式;
  • 水印配置;
  • 回调配置。

如果每次创建报表任务时都从零开始构建,代码可能非常长:

1
2
3
4
5
6
7
ReportConfig config = new ReportConfig();
config.setDataSource(...);
config.setColumns(...);
config.setPermissionRules(...);
config.setWatermarkConfig(...);
config.setExportFormat("xlsx");
config.setCallbackUrl(...);

但实际情况是,大多数报表配置都差不多,只有少数字段不同。

这时我们可以先准备一个默认模板:

1
ReportConfig defaultConfig = createDefaultReportConfig();

后续创建具体报表时,复制模板并修改差异:

1
2
3
ReportConfig orderReportConfig = defaultConfig.copy();
orderReportConfig.setReportName("订单报表");
orderReportConfig.setQueryCondition("status = PAID");

这样做有几个好处:

  • 避免重复初始化;
  • 复用复杂默认配置;
  • 创建对象更快;
  • 业务代码更清晰;
  • 可以把“模板”和“实例”区分开。

这就是原型模式的价值。

chapter 3:原型模式的核心思想

原型模式的核心思想可以用一句话概括:

先有一个可复用的模板对象,再通过复制模板创建新对象。

和其他创建型模式相比:

模式 核心关注点
简单工厂 根据类型创建对象
工厂方法 让子类决定创建哪个对象
抽象工厂 创建一整套产品族
单例模式 保证只有一个实例
建造者模式 一步步构建复杂对象
原型模式 复制已有对象创建新对象

原型模式尤其适合对象创建成本较高、结构复杂、同类对象差异较小的场景。

chapter 4:浅拷贝和深拷贝

理解原型模式,必须先理解两个概念:

  • 浅拷贝;
  • 深拷贝。

1. 浅拷贝

浅拷贝只复制对象本身的基本字段。

如果对象中包含引用类型字段,浅拷贝只会复制引用地址,不会复制引用对象本身。

例如:

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

private String username;

private Address address;

@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}

如果 address 是引用类型,那么克隆后的对象和原对象会共享同一个 Address 对象。

1
2
user1 ─────► Address
user2 ─────┘

这意味着:

1
user2.getAddress().setCity("杭州");

可能会影响 user1 的地址。

浅拷贝像复印了一张“指向房子的地图”,不是复制了一套房子。房子还是同一套。

2. 深拷贝

深拷贝不仅复制对象本身,还会复制对象内部引用的其他对象。

1
2
user1 ─────► Address1
user2 ─────► Address2

这样修改 user2 的地址,不会影响 user1

深拷贝适合对象内部包含可变引用对象,而且新旧对象之间不能互相影响的场景。

chapter 5:Java 中的 Cloneable 和 clone()

Java 提供了 Cloneable 接口和 Object.clone() 方法。

但是这个设计有点反直觉。

Cloneable 本身并没有定义 clone() 方法。

1
2
public interface Cloneable {
}

它只是一个标记接口。

真正的 clone() 方法来自 Object

1
protected native Object clone() throws CloneNotSupportedException;

如果一个类没有实现 Cloneable,直接调用 super.clone() 会抛出 CloneNotSupportedException

所以常见写法是:

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

private String username;

@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}

这种写法可以实现浅拷贝。

但是 Cloneable 在 Java 中一直有争议,原因包括:

  • Cloneable 没有定义 clone() 方法;
  • Object.clone()protected
  • 默认是浅拷贝;
  • 容易忘记处理引用类型字段;
  • 和构造方法、final 字段、继承关系配合时容易复杂;
  • 异常设计不够优雅。

所以在真实项目中,未必一定要使用 Cloneable 实现原型模式。

很多时候,拷贝构造方法、静态复制方法、Builder 复制、JSON/序列化拷贝反而更清晰。

chapter 6:实现方式一:基于 Cloneable 的浅拷贝

先看最基础的实现。

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

private String province;

private String city;

public Address(String province, String city) {
this.province = province;
this.city = city;
}

public String getProvince() {
return province;
}

public String getCity() {
return city;
}

public void setProvince(String province) {
this.province = province;
}

public void setCity(String city) {
this.city = city;
}
}

2. 定义用户类

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
public class User implements Cloneable {

private String username;

private Address address;

public User(String username, Address address) {
this.username = username;
this.address = address;
}

public String getUsername() {
return username;
}

public Address getAddress() {
return address;
}

public void setUsername(String username) {
this.username = username;
}

public void setAddress(Address address) {
this.address = address;
}

@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Clone user failed", e);
}
}
}

3. 测试浅拷贝

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

public static void main(String[] args) {
Address address = new Address("浙江省", "杭州");

User user1 = new User("mario", address);

User user2 = user1.clone();

System.out.println(user1 == user2);
System.out.println(user1.getAddress() == user2.getAddress());
}
}

输出结果:

1
2
false
true

说明:

  • user1user2 是两个不同对象;
  • 但它们内部的 address 是同一个对象。

这就是浅拷贝。

chapter 7:浅拷贝的问题

继续上面的例子。

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

public static void main(String[] args) {
Address address = new Address("浙江省", "杭州");

User user1 = new User("mario", address);

User user2 = user1.clone();

user2.getAddress().setCity("宁波");

System.out.println(user1.getAddress().getCity());
System.out.println(user2.getAddress().getCity());
}
}

输出结果:

1
2
宁波
宁波

明明只修改了 user2 的地址,结果 user1 也被影响了。

这是因为两个用户对象共享了同一个 Address 引用。

所以浅拷贝只适合以下场景:

  • 对象内部没有引用类型字段;
  • 引用类型字段是不可变对象;
  • 新旧对象允许共享内部引用;
  • 只需要复制对象最外层结构。

如果对象内部包含可变集合、配置对象、规则对象,就要小心浅拷贝。

chapter 8:实现方式二:基于 Cloneable 的深拷贝

要实现深拷贝,需要手动复制引用字段。

1. Address 支持 clone

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
public class Address implements Cloneable {

private String province;

private String city;

public Address(String province, String city) {
this.province = province;
this.city = city;
}

public String getProvince() {
return province;
}

public String getCity() {
return city;
}

public void setProvince(String province) {
this.province = province;
}

public void setCity(String city) {
this.city = city;
}

@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Clone address failed", e);
}
}
}

2. User 中手动复制 Address

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
public class User implements Cloneable {

private String username;

private Address address;

public User(String username, Address address) {
this.username = username;
this.address = address;
}

public String getUsername() {
return username;
}

public Address getAddress() {
return address;
}

public void setUsername(String username) {
this.username = username;
}

public void setAddress(Address address) {
this.address = address;
}

@Override
public User clone() {
try {
User copied = (User) super.clone();

if (this.address != null) {
copied.address = this.address.clone();
}

return copied;
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Clone user failed", e);
}
}
}

3. 测试深拷贝

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

public static void main(String[] args) {
Address address = new Address("浙江省", "杭州");

User user1 = new User("mario", address);

User user2 = user1.clone();

user2.getAddress().setCity("宁波");

System.out.println(user1.getAddress().getCity());
System.out.println(user2.getAddress().getCity());
System.out.println(user1.getAddress() == user2.getAddress());
}
}

输出结果:

1
2
3
杭州
宁波
false

这说明深拷贝成功了。

chapter 9:实现方式三:拷贝构造方法

相比 Cloneable,拷贝构造方法往往更直观。

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

private String province;

private String city;

public Address(String province, String city) {
this.province = province;
this.city = city;
}

public Address(Address source) {
this.province = source.province;
this.city = source.city;
}
}

用户类:

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

private String username;

private Address address;

public User(String username, Address address) {
this.username = username;
this.address = address;
}

public User(User source) {
this.username = source.username;
this.address = source.address == null ? null : new Address(source.address);
}
}

使用方式:

1
User user2 = new User(user1);

这种写法的优点是:

  • 不依赖 Cloneable
  • 不需要处理 CloneNotSupportedException
  • 拷贝逻辑清晰;
  • 可以显式控制深拷贝和浅拷贝;
  • 对 final 字段更加友好。

缺点是:

  • 字段多时需要写较多赋值代码;
  • 对继承体系不如 clone 灵活;
  • 新增字段时容易忘记更新拷贝构造方法。

chapter 10:实现方式四:静态复制方法

也可以提供一个静态复制方法。

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

private String username;

private Address address;

public static User copyOf(User source) {
if (source == null) {
return null;
}

Address copiedAddress = source.address == null ? null : Address.copyOf(source.address);

return new User(source.username, copiedAddress);
}

public User(String username, Address address) {
this.username = username;
this.address = address;
}
}

地址类:

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

private String province;

private String city;

public static Address copyOf(Address source) {
if (source == null) {
return null;
}

return new Address(source.province, source.city);
}

public Address(String province, String city) {
this.province = province;
this.city = city;
}
}

调用方式:

1
User copied = User.copyOf(source);

这种方式在业务代码里非常常见。

相比 clone()copyOf() 语义更明显:

我就是要复制一个对象。

chapter 11:实现方式五:Builder 复制

如果对象已经使用了 Builder,可以提供 toBuilder() 方法。

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

private final Long userId;
private final String status;
private final Integer pageNo;
private final Integer pageSize;

private OrderQuery(Builder builder) {
this.userId = builder.userId;
this.status = builder.status;
this.pageNo = builder.pageNo;
this.pageSize = builder.pageSize;
}

public Builder toBuilder() {
return new Builder()
.userId(this.userId)
.status(this.status)
.pageNo(this.pageNo)
.pageSize(this.pageSize);
}

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

public static class Builder {

private Long userId;
private String status;
private Integer pageNo = 1;
private Integer pageSize = 20;

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

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

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

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

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

使用方式:

1
2
3
4
5
6
7
8
9
OrderQuery defaultQuery = OrderQuery.builder()
.pageNo(1)
.pageSize(20)
.status("PAID")
.build();

OrderQuery userQuery = defaultQuery.toBuilder()
.userId(1001L)
.build();

这种方式很适合不可变对象。

Lombok 也支持类似能力:

1
2
3
4
5
6
7
8
9
10
11
@Builder(toBuilder = true)
public class OrderQuery {

private Long userId;

private String status;

private Integer pageNo;

private Integer pageSize;
}

调用方式:

1
2
3
OrderQuery userQuery = defaultQuery.toBuilder()
.userId(1001L)
.build();

这是实际项目中非常实用的原型思想落地方式。

chapter 12:实现方式六:序列化深拷贝

如果对象层级比较深,手动拷贝很麻烦,可以使用序列化实现深拷贝。

示例:

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 java.io.*;

public class DeepCopyUtil {

@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T source) {
if (source == null) {
return null;
}

try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(source);

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);

return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
}

实体类需要实现 Serializable

1
2
3
4
5
6
7
8
9
10
import java.io.Serializable;

public class Address implements Serializable {

private static final long serialVersionUID = 1L;

private String province;

private String city;
}
1
2
3
4
5
6
7
8
9
10
import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 1L;

private String username;

private Address address;
}

使用方式:

1
User copied = DeepCopyUtil.deepCopy(source);

序列化深拷贝的优点

  • 可以自动复制较深的对象图;
  • 不需要每个字段手动复制;
  • 适合临时工具场景。

序列化深拷贝的缺点

  • 性能较差;
  • 所有相关类都要实现 Serializable
  • 对 transient 字段不友好;
  • 对复杂对象图可能有坑;
  • 不适合高频调用。

所以它不是日常首选方案,更适合低频、复杂对象、工具类场景。

chapter 13:实现方式七:JSON 深拷贝

在业务项目中,有些人会用 JSON 序列化再反序列化实现深拷贝。

例如使用 Jackson:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonCopyUtil {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

public static <T> T deepCopy(T source, Class<T> targetClass) {
try {
String json = OBJECT_MAPPER.writeValueAsString(source);

return OBJECT_MAPPER.readValue(json, targetClass);
} catch (Exception e) {
throw new RuntimeException("Json deep copy failed", e);
}
}
}

使用方式:

1
User copied = JsonCopyUtil.deepCopy(source, User.class);

优点

  • 使用简单;
  • 不需要实现 Serializable
  • 对普通 DTO 比较方便;
  • 适合跨层对象复制或测试场景。

缺点

  • 性能不高;
  • 需要默认构造方法和 getter/setter;
  • 泛型、时间类型、多态类型可能需要额外配置;
  • 字段注解会影响结果;
  • 不适合复杂领域对象。

JSON 深拷贝是个工具,不是银弹。

别把对象复制写成“对象出国旅游一圈再回来”。能手动清晰复制,就别绕太远。

chapter 14:原型注册表

原型模式还有一种常见扩展:原型注册表。

它的核心思想是:

把多个原型对象提前注册到一个容器中,后续按类型获取并复制。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.HashMap;
import java.util.Map;

public class ReportTemplateRegistry {

private final Map<String, ReportConfig> templateMap = new HashMap<>();

public void register(String type, ReportConfig config) {
templateMap.put(type, config);
}

public ReportConfig getCopy(String type) {
ReportConfig prototype = templateMap.get(type);

if (prototype == null) {
throw new IllegalArgumentException("Unsupported report type: " + type);
}

return prototype.copy();
}
}

使用方式:

1
2
3
4
5
6
7
ReportTemplateRegistry registry = new ReportTemplateRegistry();

registry.register("order", orderReportTemplate);
registry.register("payment", paymentReportTemplate);

ReportConfig config = registry.getCopy("order");
config.setReportName("今日订单报表");

这种方式适合:

  • 模板配置;
  • 规则配置;
  • 流程配置;
  • 游戏对象模板;
  • 表单模板;
  • 报表模板;
  • 消息模板。

chapter 15:案例背景:报表导出配置模板

下面用一个 Java 后端真实一点的案例来讲原型模式。

假设我们有一个报表导出系统。

不同报表有很多相同配置:

  • 默认导出格式;
  • 默认分页大小;
  • 默认水印;
  • 默认权限控制;
  • 默认排序字段;
  • 默认通知方式;
  • 默认存储路径。

但是每个具体报表又有少量差异:

  • 报表名称不同;
  • 查询条件不同;
  • 导出字段不同;
  • 文件名前缀不同。

如果每次都从零构建配置,会有大量重复代码。

所以我们可以:

  1. 初始化几个报表模板;
  2. 业务执行时复制模板;
  3. 修改少量差异字段;
  4. 使用复制后的配置执行导出。

chapter 16:定义报表配置类

先定义报表配置。

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
import java.util.ArrayList;
import java.util.List;

public class ReportConfig {

private String reportName;

private String exportFormat;

private Integer pageSize;

private String sortField;

private WatermarkConfig watermarkConfig;

private List<String> columns;

public ReportConfig() {
}

private ReportConfig(ReportConfig source) {
this.reportName = source.reportName;
this.exportFormat = source.exportFormat;
this.pageSize = source.pageSize;
this.sortField = source.sortField;
this.watermarkConfig = source.watermarkConfig == null
? null
: new WatermarkConfig(source.watermarkConfig);
this.columns = source.columns == null
? new ArrayList<>()
: new ArrayList<>(source.columns);
}

public ReportConfig copy() {
return new ReportConfig(this);
}

public String getReportName() {
return reportName;
}

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

public String getExportFormat() {
return exportFormat;
}

public void setExportFormat(String exportFormat) {
this.exportFormat = exportFormat;
}

public Integer getPageSize() {
return pageSize;
}

public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}

public String getSortField() {
return sortField;
}

public void setSortField(String sortField) {
this.sortField = sortField;
}

public WatermarkConfig getWatermarkConfig() {
return watermarkConfig;
}

public void setWatermarkConfig(WatermarkConfig watermarkConfig) {
this.watermarkConfig = watermarkConfig;
}

public List<String> getColumns() {
return columns;
}

public void setColumns(List<String> columns) {
this.columns = columns;
}
}

注意这里使用了拷贝构造方法,并且对 watermarkConfigcolumns 做了复制。

这是一种手写深拷贝。

chapter 17:定义水印配置类

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

private String text;

private Integer opacity;

public WatermarkConfig(String text, Integer opacity) {
this.text = text;
this.opacity = opacity;
}

public WatermarkConfig(WatermarkConfig source) {
this.text = source.text;
this.opacity = source.opacity;
}

public String getText() {
return text;
}

public Integer getOpacity() {
return opacity;
}

public void setText(String text) {
this.text = text;
}

public void setOpacity(Integer opacity) {
this.opacity = opacity;
}
}

chapter 18:定义模板注册表

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
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReportTemplateRegistry {

private final Map<String, ReportConfig> templateMap = new HashMap<>();

public ReportTemplateRegistry() {
initTemplates();
}

private void initTemplates() {
ReportConfig orderTemplate = new ReportConfig();
orderTemplate.setReportName("默认订单报表");
orderTemplate.setExportFormat("xlsx");
orderTemplate.setPageSize(1000);
orderTemplate.setSortField("create_time");
orderTemplate.setWatermarkConfig(new WatermarkConfig("内部资料", 30));
orderTemplate.setColumns(List.of("orderId", "userId", "amount", "status", "createTime"));

ReportConfig paymentTemplate = new ReportConfig();
paymentTemplate.setReportName("默认支付报表");
paymentTemplate.setExportFormat("xlsx");
paymentTemplate.setPageSize(1000);
paymentTemplate.setSortField("pay_time");
paymentTemplate.setWatermarkConfig(new WatermarkConfig("财务数据", 40));
paymentTemplate.setColumns(List.of("paymentId", "orderId", "amount", "channel", "payTime"));

templateMap.put("order", orderTemplate);
templateMap.put("payment", paymentTemplate);
}

public ReportConfig getTemplateCopy(String type) {
ReportConfig template = templateMap.get(type);

if (template == null) {
throw new IllegalArgumentException("Unsupported report type: " + type);
}

return template.copy();
}
}

这个注册表保存的是原型对象。

调用方拿到的不是原型本身,而是原型的复制品。

这样就避免调用方修改模板对象。

chapter 19:使用原型创建具体报表配置

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

private final ReportTemplateRegistry registry = new ReportTemplateRegistry();

public void exportOrderReport() {
ReportConfig config = registry.getTemplateCopy("order");

config.setReportName("今日订单报表");
config.getColumns().add("remark");

doExport(config);
}

public void exportPaymentReport() {
ReportConfig config = registry.getTemplateCopy("payment");

config.setReportName("本月支付报表");

doExport(config);
}

private void doExport(ReportConfig config) {
System.out.println("开始导出报表:" + config.getReportName());
System.out.println("导出格式:" + config.getExportFormat());
System.out.println("导出字段:" + config.getColumns());
System.out.println("水印内容:" + config.getWatermarkConfig().getText());
}
}

这里的关键点是:

1
ReportConfig config = registry.getTemplateCopy("order");

业务层并不是从零创建配置,而是复制一个模板。

这就是原型模式的典型用法。

chapter 20:如果这里用浅拷贝会出什么问题

假设 copy() 方法只是浅拷贝:

1
2
3
4
5
6
7
8
9
10
public ReportConfig copy() {
ReportConfig copied = new ReportConfig();
copied.reportName = this.reportName;
copied.exportFormat = this.exportFormat;
copied.pageSize = this.pageSize;
copied.sortField = this.sortField;
copied.watermarkConfig = this.watermarkConfig;
copied.columns = this.columns;
return copied;
}

然后业务代码这样写:

1
2
3
ReportConfig config = registry.getTemplateCopy("order");

config.getColumns().add("remark");

如果是浅拷贝,columns 列表是共享的。

那么模板对象中的字段列表也会被修改。

下次再复制订单报表模板时,可能会发现 remark 字段已经在模板里了。

这就是非常典型的原型模式踩坑:

你以为自己复制了对象,其实只是复制了一个对象壳。

所以在配置模板、规则模板、流程模板场景中,通常要特别注意深拷贝。

chapter 21:Spring 中的 prototype scope

说到原型模式,很多人会想到 Spring 的 prototype 作用域。

Spring 默认 Bean 是单例:

1
2
3
@Component
public class OrderService {
}

默认情况下,一个 Spring 容器里只有一个 OrderService Bean。

如果设置为原型作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Scope("prototype")
public class TaskContext {

private String taskId;

private String operator;

public String getTaskId() {
return taskId;
}

public void setTaskId(String taskId) {
this.taskId = taskId;
}

public String getOperator() {
return operator;
}

public void setOperator(String operator) {
this.operator = operator;
}
}

每次从 Spring 容器中获取时,都会创建一个新对象。

1
2
3
4
TaskContext context1 = applicationContext.getBean(TaskContext.class);
TaskContext context2 = applicationContext.getBean(TaskContext.class);

System.out.println(context1 == context2);

输出:

1
false

不过要注意:

Spring 的 prototype scope 和 GoF 原型模式不是完全一回事。

Spring prototype 的意思是每次获取 Bean 时创建一个新实例。

GoF 原型模式强调的是通过复制已有原型对象创建新实例。

两者名字相似,思想上都和“多个实例”有关,但实现机制不完全一样。

chapter 22:Spring prototype 的常见坑

1. 单例 Bean 注入 prototype Bean

如果把 prototype Bean 注入到 singleton Bean 中:

1
2
3
4
5
6
7
8
9
@Service
public class TaskService {

private final TaskContext taskContext;

public TaskService(TaskContext taskContext) {
this.taskContext = taskContext;
}
}

由于 TaskService 是单例,它只会在创建时注入一次 TaskContext

后续每次使用的仍然是同一个 TaskContext 实例。

也就是说,prototype 的效果失效了。

2. 正确方式:ObjectProvider

可以使用 ObjectProvider 每次获取新实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class TaskService {

private final ObjectProvider<TaskContext> taskContextProvider;

public TaskService(ObjectProvider<TaskContext> taskContextProvider) {
this.taskContextProvider = taskContextProvider;
}

public void executeTask(String taskId, String operator) {
TaskContext context = taskContextProvider.getObject();

context.setTaskId(taskId);
context.setOperator(operator);

System.out.println("执行任务:" + context.getTaskId());
}
}

这样每次调用 getObject(),都会获取一个新的 prototype Bean。

3. 也可以使用 @Lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public abstract class TaskService {

public void executeTask(String taskId, String operator) {
TaskContext context = createTaskContext();

context.setTaskId(taskId);
context.setOperator(operator);

System.out.println("执行任务:" + context.getTaskId());
}

@Lookup
protected abstract TaskContext createTaskContext();
}

Spring 会通过方法注入的方式,在每次调用时返回新的 prototype Bean。

不过在实际项目中,ObjectProvider 更直观。

chapter 23:原型模式和建造者模式的区别

原型模式和建造者模式都能创建复杂对象,但关注点不同。

对比项 原型模式 建造者模式
创建方式 复制已有对象 一步步构建对象
适合场景 对象已有模板,差异较小 参数很多,构建过程复杂
重点 复用已有状态 清晰表达构建过程
典型方法 clone、copy、copyOf builder、build
风险 浅拷贝共享引用 忘记校验参数
示例 复制报表模板 构建 HTTP 请求对象

简单说:

如果你已经有一个模板对象,并且新对象和它大部分一样,用原型模式。

如果你要从一堆参数一步步构建一个新对象,用建造者模式。

两者也可以结合使用。

例如:

1
2
3
OrderQuery newQuery = oldQuery.toBuilder()
.status("PAID")
.build();

这其实就是 Builder 风格的原型复制。

chapter 24:原型模式和工厂模式的区别

工厂模式通过工厂创建对象。

原型模式通过复制已有对象创建对象。

对比项 工厂模式 原型模式
创建来源 类或工厂逻辑 已有对象
是否依赖已有实例 不依赖 依赖
适合场景 根据类型创建不同对象 复制模板对象
初始化逻辑 通常写在构造/工厂中 原型对象已完成初始化
示例 PaymentFactory.create(type) reportTemplate.copy()

如果对象初始化非常复杂,而且很多对象只是基于模板做少量变化,原型模式会比工厂模式更自然。

chapter 25:原型模式的优点

1. 提高对象创建效率

如果对象初始化成本较高,通过复制已有对象可能更快。

2. 避免重复初始化代码

复杂默认配置可以沉淀到模板对象中。

3. 动态创建对象

可以在运行时注册不同原型,并按需复制。

4. 降低创建对象的复杂度

客户端不需要知道对象内部如何初始化,只需要复制原型即可。

5. 适合模板化业务

例如报表模板、消息模板、规则模板、流程模板。

chapter 26:原型模式的缺点

1. 深拷贝复杂

如果对象内部结构复杂,深拷贝会比较麻烦。

2. 容易误用浅拷贝

浅拷贝共享引用对象,可能导致原型对象被意外污染。

3. Cloneable 设计不够优雅

Java 的 Cloneable 使用体验并不好,容易踩坑。

4. 对象图复杂时难以维护

如果对象之间存在循环引用、共享引用、资源句柄,复制逻辑会很复杂。

5. 复制资源对象可能有风险

例如数据库连接、线程池、文件句柄、Socket 连接,不应该简单复制。

chapter 27:适用场景

原型模式适合以下场景。

1. 对象创建成本较高

例如初始化时需要加载大量配置或计算默认值。

2. 对象之间差异较小

例如多个报表配置大部分相同,只有少数字段不同。

3. 需要保存多个对象模板

例如:

  • 表单模板;
  • 报表模板;
  • 消息模板;
  • 流程模板;
  • 权限规则模板。

4. 希望避免复杂构造逻辑暴露给客户端

客户端只需要复制模板,不需要知道模板怎么构建出来。

5. 运行时动态扩展模板

可以通过注册表在运行时新增或替换原型对象。

chapter 28:不适合使用的场景

以下场景不建议使用原型模式。

1. 对象很简单

如果对象只有几个字段,直接构造就行。

1
new User(id, name)

没必要先搞一个原型再复制。

2. 对象内部包含不可复制资源

例如:

  • 数据库连接;
  • 线程池;
  • 文件流;
  • Socket;
  • 锁对象;
  • 事务上下文。

这些对象不能简单复制。

3. 对象状态强依赖外部上下文

例如当前请求上下文、当前登录用户、当前事务状态。

这类对象复制后可能语义不正确。

4. 深拷贝成本过高

如果对象图非常大,深拷贝可能比重新创建还贵。

不要为了“模式正确”牺牲性能和清晰度。

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

1. 优先明确是浅拷贝还是深拷贝

复制方法命名最好体现语义。

1
2
3
4
copy()
shallowCopy()
deepCopy()
copyOf()

不要让调用者猜。

2. 模板对象尽量不可变

如果原型对象作为模板保存,最好减少可变状态。

例如使用不可变集合:

1
List.copyOf(columns)

这样可以降低模板被误修改的风险。

3. 不要复制资源对象

连接、线程池、锁、文件句柄这类资源对象不要放进原型复制逻辑里。

它们应该由资源管理器或容器管理。

4. 复杂深拷贝要写测试

深拷贝逻辑非常容易遗漏字段。

新增字段后,要检查复制逻辑是否同步更新。

5. Spring 项目中不要滥用 prototype Bean

如果只是请求级状态,优先考虑方法局部变量、请求对象、上下文对象,而不是到处使用 prototype scope。

6. DTO 可以使用 copyOf,领域对象要谨慎

普通 DTO、配置对象、模板对象适合复制。

但领域对象通常有业务不变量和生命周期,复制时要非常谨慎。

例如订单对象不能随便复制一个新订单出来,否则状态、明细、支付记录、库存关系都可能乱。

领域对象不是 Word 文档,不能看到顺眼就 Ctrl+C、Ctrl+V。

chapter 30:完整案例代码汇总

水印配置

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

private String text;

private Integer opacity;

public WatermarkConfig(String text, Integer opacity) {
this.text = text;
this.opacity = opacity;
}

public WatermarkConfig(WatermarkConfig source) {
this.text = source.text;
this.opacity = source.opacity;
}

public String getText() {
return text;
}

public Integer getOpacity() {
return opacity;
}

public void setText(String text) {
this.text = text;
}

public void setOpacity(Integer opacity) {
this.opacity = opacity;
}
}

报表配置

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
import java.util.ArrayList;
import java.util.List;

public class ReportConfig {

private String reportName;

private String exportFormat;

private Integer pageSize;

private String sortField;

private WatermarkConfig watermarkConfig;

private List<String> columns;

public ReportConfig() {
}

private ReportConfig(ReportConfig source) {
this.reportName = source.reportName;
this.exportFormat = source.exportFormat;
this.pageSize = source.pageSize;
this.sortField = source.sortField;
this.watermarkConfig = source.watermarkConfig == null
? null
: new WatermarkConfig(source.watermarkConfig);
this.columns = source.columns == null
? new ArrayList<>()
: new ArrayList<>(source.columns);
}

public ReportConfig copy() {
return new ReportConfig(this);
}

public String getReportName() {
return reportName;
}

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

public String getExportFormat() {
return exportFormat;
}

public void setExportFormat(String exportFormat) {
this.exportFormat = exportFormat;
}

public Integer getPageSize() {
return pageSize;
}

public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}

public String getSortField() {
return sortField;
}

public void setSortField(String sortField) {
this.sortField = sortField;
}

public WatermarkConfig getWatermarkConfig() {
return watermarkConfig;
}

public void setWatermarkConfig(WatermarkConfig watermarkConfig) {
this.watermarkConfig = watermarkConfig;
}

public List<String> getColumns() {
return columns;
}

public void setColumns(List<String> columns) {
this.columns = columns;
}
}

模板注册表

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
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReportTemplateRegistry {

private final Map<String, ReportConfig> templateMap = new HashMap<>();

public ReportTemplateRegistry() {
initTemplates();
}

private void initTemplates() {
ReportConfig orderTemplate = new ReportConfig();
orderTemplate.setReportName("默认订单报表");
orderTemplate.setExportFormat("xlsx");
orderTemplate.setPageSize(1000);
orderTemplate.setSortField("create_time");
orderTemplate.setWatermarkConfig(new WatermarkConfig("内部资料", 30));
orderTemplate.setColumns(List.of("orderId", "userId", "amount", "status", "createTime"));

ReportConfig paymentTemplate = new ReportConfig();
paymentTemplate.setReportName("默认支付报表");
paymentTemplate.setExportFormat("xlsx");
paymentTemplate.setPageSize(1000);
paymentTemplate.setSortField("pay_time");
paymentTemplate.setWatermarkConfig(new WatermarkConfig("财务数据", 40));
paymentTemplate.setColumns(List.of("paymentId", "orderId", "amount", "channel", "payTime"));

templateMap.put("order", orderTemplate);
templateMap.put("payment", paymentTemplate);
}

public ReportConfig getTemplateCopy(String type) {
ReportConfig template = templateMap.get(type);

if (template == null) {
throw new IllegalArgumentException("Unsupported report type: " + type);
}

return template.copy();
}
}

导出服务

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

private final ReportTemplateRegistry registry = new ReportTemplateRegistry();

public void exportOrderReport() {
ReportConfig config = registry.getTemplateCopy("order");

config.setReportName("今日订单报表");
config.getColumns().add("remark");

doExport(config);
}

public void exportPaymentReport() {
ReportConfig config = registry.getTemplateCopy("payment");

config.setReportName("本月支付报表");

doExport(config);
}

private void doExport(ReportConfig config) {
System.out.println("开始导出报表:" + config.getReportName());
System.out.println("导出格式:" + config.getExportFormat());
System.out.println("导出字段:" + config.getColumns());
System.out.println("水印内容:" + config.getWatermarkConfig().getText());
}
}

客户端

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

public static void main(String[] args) {
ReportExportService service = new ReportExportService();

service.exportOrderReport();
service.exportPaymentReport();
}
}

chapter 31:一句话总结

原型模式的本质是:

通过复制已有对象来创建新对象,尤其适合对象创建成本高、模板化程度高、对象之间差异较小的场景。

它的关键不在于会不会写 clone(),而在于你是否理解:

  • 什么对象适合复制;
  • 什么对象不能复制;
  • 什么时候用浅拷贝;
  • 什么时候必须用深拷贝;
  • 如何避免原型对象被污染;
  • 如何在 Spring 项目中正确理解 prototype scope。

如果说建造者模式是“按步骤搭积木”,那么原型模式就是“复制一个已有模型再改细节”。

能复制当然省事,但复制前要确认:你复制的是对象,不是问题。

参考资料

  • 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: Bean Scopes.
  • Oracle Java Documentation: Object.clone().
  • Refactoring Guru: Prototype Pattern.
  • SourceMaking: Prototype Design Pattern.

启示录

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

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


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