欢迎你来读这篇博客,这篇博客主要是关于原型模式。
其中包括原型模式的核心思想、适用场景、浅拷贝与深拷贝、Cloneable 的问题、序列化拷贝、Spring 原型 Bean,以及 Java 后端开发中的真实案例。
序言
前面已经聊过简单工厂、工厂方法、抽象工厂、单例和建造者模式。
这些模式都在解决一个问题:
对象应该如何创建?
但是原型模式的思路有点不一样。
它不是从零开始创建对象,而是:
通过复制一个已有对象,快速创建一个新对象。
也就是说,原型模式关注的不是 new,而是 copy。
在真实项目中,有些对象创建过程非常复杂:
- 初始化字段很多;
- 需要读取配置;
- 需要计算默认值;
- 需要组装规则;
- 需要加载模板;
- 需要进行较重的初始化逻辑。
如果每次都重新创建,会很麻烦,也可能影响性能。
这时就可以先准备一个“原型对象”,后续需要新对象时,直接基于原型复制一份,再修改少量差异字段。
这就像写文章时不会每次都从空白页开始,而是复制一个模板再改。程序员懒得很高级时,就叫设计模式。
正文
chapter 1:什么是原型模式
原型模式,英文是 Prototype Pattern,属于创建型设计模式。
它的定义是:
使用原型实例指定要创建对象的种类,并通过复制这个原型来创建新的对象。
简单说:
原型模式就是通过复制已有对象来创建新对象。
原型模式一般包含三个角色:
- Prototype 抽象原型:定义克隆方法。
- ConcretePrototype 具体原型:实现克隆方法。
- Client 客户端:通过克隆原型对象创建新对象。
典型结构如下:
1 2 3 4 5 6 7 8
| Client │ ▼ Prototype │ clone() ▲ │ ConcretePrototype
|
在 Java 中,原型模式经常会和 Cloneable、clone()、拷贝构造方法、序列化、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()); } }
|
输出结果:
说明:
user1 和 user2 是两个不同对象;
- 但它们内部的
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()); } }
|
输出结果:
明明只修改了 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()); } }
|
输出结果:
这说明深拷贝成功了。
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 后端真实一点的案例来讲原型模式。
假设我们有一个报表导出系统。
不同报表有很多相同配置:
- 默认导出格式;
- 默认分页大小;
- 默认水印;
- 默认权限控制;
- 默认排序字段;
- 默认通知方式;
- 默认存储路径。
但是每个具体报表又有少量差异:
- 报表名称不同;
- 查询条件不同;
- 导出字段不同;
- 文件名前缀不同。
如果每次都从零构建配置,会有大量重复代码。
所以我们可以:
- 初始化几个报表模板;
- 业务执行时复制模板;
- 修改少量差异字段;
- 使用复制后的配置执行导出。
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; } }
|
注意这里使用了拷贝构造方法,并且对 watermarkConfig 和 columns 做了复制。
这是一种手写深拷贝。
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);
|
输出:
不过要注意:
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. 对象很简单
如果对象只有几个字段,直接构造就行。
没必要先搞一个原型再复制。
2. 对象内部包含不可复制资源
例如:
- 数据库连接;
- 线程池;
- 文件流;
- Socket;
- 锁对象;
- 事务上下文。
这些对象不能简单复制。
3. 对象状态强依赖外部上下文
例如当前请求上下文、当前登录用户、当前事务状态。
这类对象复制后可能语义不正确。
4. 深拷贝成本过高
如果对象图非常大,深拷贝可能比重新创建还贵。
不要为了“模式正确”牺牲性能和清晰度。
chapter 29:真实项目中的实践建议
1. 优先明确是浅拷贝还是深拷贝
复制方法命名最好体现语义。
1 2 3 4
| copy() shallowCopy() deepCopy() copyOf()
|
不要让调用者猜。
2. 模板对象尽量不可变
如果原型对象作为模板保存,最好减少可变状态。
例如使用不可变集合:
这样可以降低模板被误修改的风险。
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.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。