原型模式:深拷贝与浅拷贝,Spring Bean scope=prototype的内存分析
原型模式:深拷贝与浅拷贝,Spring Bean scope=prototype的内存分析
适读人群:中高级Java开发者 | 阅读时长:约22分钟 | 模式类型:创建型
开篇故事
2022年,我们的电商平台在大促期间出现了一个非常诡异的 bug:不同用户的购物车里,居然出现了相同的商品条目。用户A选了一个SKU,用户B的购物车里也出现了这个SKU,而用户B明明没有做任何操作。
排查了很久,最终定位到这个问题的根因是:我们有一个"模板购物车"功能,允许用户把自己的购物车保存为模板,其他用户可以"一键复制"这个模板。技术实现上,我们用了一个对象拷贝的方法,但用的是浅拷贝,导致所有从同一模板复制出来的购物车,其内部的 List<CartItem> 引用的是同一个列表对象。
用户A修改了自己购物车里的商品,实际上修改的是那个共享的列表,所有其他用户的购物车同步受到了影响。
这个 bug 让我对浅拷贝和深拷贝有了刻骨铭心的认识。今天结合原型模式,把这个话题讲透彻。
一、模式动机:为什么需要原型模式
原型模式(Prototype Pattern)的核心诉求:通过复制(克隆)一个已有对象来创建新对象,而不是每次都从头 new 一个。
适用场景:
- 对象创建成本高:对象的初始化需要大量计算或 I/O 操作(比如从数据库加载大量数据来初始化),克隆比重新创建要快得多。
- 对象状态需要保留并在此基础上修改:比如文档编辑中的"另存为",游戏中的"存档"和"读档"。
- 避免工厂方法模式的子类层次:当需要独立于产品类来使用时,原型可以避免大量子类。
- Spring scope=prototype:每次从容器获取 Bean 时,都返回一个新的实例,这就是原型模式的直接应用。
二、模式结构
Java 中原型模式的核心是 Cloneable 接口和 Object.clone() 方法。
三、浅拷贝 vs 深拷贝的底层原理
3.1 Java 内存模型与引用
理解深浅拷贝,必须先理解 Java 内存模型:
- 基本类型(int, long, double, boolean 等):直接存储值
- 引用类型(对象、数组):存储的是堆内存中对象的地址(引用)
栈内存:
┌─────────────────────────┐
│ CartTemplate template │──────────────────────┐
│ CartTemplate copy │──────────────────────│──→
└─────────────────────────┘ │ │
↓ ↓
堆内存: ┌─────────────────────────────┐
│ CartTemplate原始对象 │
│ userId: 1001 │
│ items ──────────────────→ [List<CartItem>] ←── 浅拷贝的copy也指向这里!
└─────────────────────────────┘浅拷贝:拷贝对象本身,但内部的引用类型字段仍然指向同一块内存地址。 深拷贝:递归拷贝所有字段,包括引用类型字段指向的对象,产生完全独立的副本。
3.2 Java Object.clone() 的实现
Object.clone() 是 native 方法,它实现的是浅拷贝:
protected native Object clone() throws CloneNotSupportedException;要使用 clone(),必须:
- 实现
Cloneable接口(标记接口,无方法) - 覆盖
clone()方法并改为public
public class ShallowCopyDemo implements Cloneable {
private int id;
private String name; // String是不可变的,浅拷贝安全
private List<String> tags; // 可变集合,浅拷贝不安全!
@Override
public ShallowCopyDemo clone() {
try {
return (ShallowCopyDemo) super.clone(); // 浅拷贝
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 不会发生,因为实现了Cloneable
}
}
}四、生产级代码实现
4.1 完整的购物车克隆实现(含深拷贝)
/**
* 购物车商品条目
*/
@Data
@AllArgsConstructor
public class CartItem implements Cloneable {
private String skuId;
private String productName;
private BigDecimal price;
private int quantity;
private Map<String, String> attributes; // 商品属性,如颜色、尺码
@Override
public CartItem clone() {
try {
CartItem cloned = (CartItem) super.clone();
// 深拷贝attributes(Map是可变的)
if (this.attributes != null) {
cloned.attributes = new HashMap<>(this.attributes);
}
// BigDecimal是不可变的,String是不可变的,int是基本类型,无需特殊处理
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError("Clone not supported for CartItem", e);
}
}
}
/**
* 购物车模板(支持深拷贝的原型)
*/
@Data
public class CartTemplate implements Cloneable {
private Long templateId;
private Long ownerId;
private String templateName;
private List<CartItem> items;
private Map<String, String> metadata;
private LocalDateTime createdAt;
/**
* 深拷贝实现:复制购物车模板给其他用户使用
* 必须是完全独立的副本,避免共享状态
*/
@Override
public CartTemplate clone() {
try {
CartTemplate cloned = (CartTemplate) super.clone();
// 深拷贝 items 列表(每个CartItem也需要深拷贝)
if (this.items != null) {
cloned.items = this.items.stream()
.map(CartItem::clone) // CartItem也实现了clone()
.collect(Collectors.toCollection(ArrayList::new));
}
// 深拷贝 metadata Map
if (this.metadata != null) {
cloned.metadata = new HashMap<>(this.metadata);
}
// LocalDateTime是不可变的,无需深拷贝
// Long, String 也是不可变的
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError("Clone not supported for CartTemplate", e);
}
}
/**
* 基于模板创建用户购物车
* 这是原型模式的应用:通过克隆模板来创建新购物车
*/
public UserCart toUserCart(Long userId) {
CartTemplate copy = this.clone(); // 深拷贝
return UserCart.builder()
.userId(userId)
.items(copy.getItems()) // 使用深拷贝的items,与模板完全独立
.fromTemplateId(this.templateId)
.createdAt(LocalDateTime.now())
.build();
}
}4.2 序列化方式实现深拷贝(通用方案)
对于复杂对象图,手写 clone() 非常容易出错(忘记深拷贝某个字段)。序列化方式是更可靠的通用深拷贝方案:
/**
* 深拷贝工具类(序列化方式)
*/
public final class DeepCopyUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
/**
* 通过 JSON 序列化/反序列化实现深拷贝
* 优点:简单可靠,不需要实现Cloneable
* 缺点:性能开销较大,需要无参构造函数,不支持循环引用
*/
@SuppressWarnings("unchecked")
public static <T> T deepCopyViaJson(T original) {
if (original == null) return null;
try {
String json = OBJECT_MAPPER.writeValueAsString(original);
return (T) OBJECT_MAPPER.readValue(json, original.getClass());
} catch (JsonProcessingException e) {
throw new RuntimeException("Deep copy via JSON failed", e);
}
}
/**
* 通过Java对象序列化实现深拷贝
* 优点:支持所有实现Serializable的类
* 缺点:性能最差,对象必须实现Serializable
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopyViaSerialization(T original) {
if (original == null) return null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(original);
}
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()))) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy via serialization failed", e);
}
}
/**
* 深拷贝 List(推荐用于频繁拷贝场景,比JSON方式快)
*/
public static <T> List<T> deepCopyList(List<T> original, Function<T, T> itemCopier) {
if (original == null) return null;
return original.stream()
.map(itemCopier)
.collect(Collectors.toCollection(ArrayList::new));
}
}4.3 原型注册表(Prototype Registry)
在需要管理多个原型的场景,可以用原型注册表来集中管理:
/**
* 消息模板原型注册表
* 存储各种消息模板的原型,需要时通过克隆获取新实例
*/
@Component
@Slf4j
public class MessageTemplateRegistry {
private final Map<String, MessageTemplate> prototypes = new ConcurrentHashMap<>();
@Autowired
private MessageTemplateRepository repository;
@PostConstruct
public void loadPrototypes() {
// 从数据库加载所有活跃的消息模板作为原型
List<MessageTemplate> templates = repository.findAllActive();
templates.forEach(t -> prototypes.put(t.getTemplateCode(), t));
log.info("Loaded {} message template prototypes", templates.size());
}
/**
* 获取模板的克隆(不是原型本身,防止原型被修改)
*/
public MessageTemplate getTemplate(String templateCode) {
MessageTemplate prototype = prototypes.get(templateCode);
if (prototype == null) {
throw new IllegalArgumentException("Template not found: " + templateCode);
}
return prototype.clone(); // 返回克隆,不是原型本身
}
/**
* 注册新原型
*/
public void registerPrototype(String code, MessageTemplate template) {
prototypes.put(code, template.clone()); // 存储克隆,防止外部修改影响原型
log.info("Registered message template prototype: {}", code);
}
/**
* 刷新特定原型(数据库更新后)
*/
public void refreshPrototype(String templateCode) {
MessageTemplate template = repository.findByCode(templateCode)
.orElseThrow(() -> new IllegalArgumentException("Template not found: " + templateCode));
prototypes.put(templateCode, template);
log.info("Refreshed message template prototype: {}", templateCode);
}
}
/**
* 消息模板(支持克隆的原型)
*/
@Data
public class MessageTemplate implements Cloneable {
private String templateCode;
private String templateName;
private String subject;
private String bodyTemplate; // 包含占位符的模板字符串,如 "您好${username},您的订单${orderId}已发货"
private Map<String, String> defaultParams; // 默认参数
private MessageChannel channel; // EMAIL, SMS, PUSH
private boolean htmlEnabled;
/**
* 克隆模板,并用实际参数替换占位符
*/
public MessageTemplate cloneWithParams(Map<String, String> params) {
MessageTemplate cloned = this.clone();
// 用参数替换占位符
String processedBody = cloned.bodyTemplate;
for (Map.Entry<String, String> entry : params.entrySet()) {
processedBody = processedBody.replace("${" + entry.getKey() + "}", entry.getValue());
}
// 也替换默认参数中的占位符
for (Map.Entry<String, String> entry : cloned.defaultParams.entrySet()) {
processedBody = processedBody.replace("${" + entry.getKey() + "}", entry.getValue());
}
cloned.bodyTemplate = processedBody;
return cloned;
}
@Override
public MessageTemplate clone() {
try {
MessageTemplate cloned = (MessageTemplate) super.clone();
if (this.defaultParams != null) {
cloned.defaultParams = new HashMap<>(this.defaultParams);
}
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}4.4 Spring scope=prototype 的深度分析
/**
* Spring scope=prototype 的实际使用场景
* 注意:prototype Bean 不受 Spring 生命周期管理(Spring不会调用其destroy方法)
*/
// 场景一:有状态的Bean(每个使用者需要独立状态)
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
public class OrderProcessor {
private Order currentOrder; // 有状态!每个请求需要独立的处理器实例
private List<String> processLogs = new ArrayList<>();
private ProcessStatus status = ProcessStatus.PENDING;
public void setOrder(Order order) {
this.currentOrder = order;
this.processLogs.clear();
this.status = ProcessStatus.PENDING;
}
public ProcessResult process() {
try {
validateOrder();
calculateDiscount();
reserveInventory();
createPaymentRecord();
status = ProcessStatus.SUCCESS;
} catch (Exception e) {
status = ProcessStatus.FAILED;
processLogs.add("ERROR: " + e.getMessage());
throw e;
}
return new ProcessResult(status, processLogs);
}
private void validateOrder() {
processLogs.add("Validating order: " + currentOrder.getId());
// 验证逻辑...
}
private void calculateDiscount() {
processLogs.add("Calculating discount...");
// 折扣计算...
}
// ... 其他步骤方法
}
/**
* 在单例Bean中注入prototype Bean 的正确方式
* 坑:直接用@Autowired注入prototype Bean,实际上只会注入一次,
* 每次使用的是同一个实例,prototype退化成singleton
*/
@Service
public class OrderService {
// 错误方式:这样注入的OrderProcessor只有一个,不是每次都新建
// @Autowired
// private OrderProcessor orderProcessor; // 错误!
// 正确方式一:注入ApplicationContext,每次手动getBean
@Autowired
private ApplicationContext applicationContext;
public ProcessResult processOrder(Order order) {
// 每次调用都从容器获取新的prototype实例
OrderProcessor processor = applicationContext.getBean(OrderProcessor.class);
processor.setOrder(order);
return processor.process();
}
// 正确方式二:使用 Provider<T>(JSR-330标准)
@Autowired
private Provider<OrderProcessor> orderProcessorProvider;
public ProcessResult processOrderV2(Order order) {
OrderProcessor processor = orderProcessorProvider.get(); // 每次获取新实例
processor.setOrder(order);
return processor.process();
}
// 正确方式三:使用 @Lookup 方法注入(Spring特有)
// Spring会在运行时覆盖这个方法,让它每次返回新的prototype实例
@Lookup
protected OrderProcessor createOrderProcessor() {
return null; // 方法体无关紧要,Spring会覆盖它
}
public ProcessResult processOrderV3(Order order) {
OrderProcessor processor = createOrderProcessor(); // 实际调用Spring覆盖的版本
processor.setOrder(order);
return processor.process();
}
}4.5 prototype Bean 的内存分析
/**
* prototype Bean内存分析:监控内存使用情况
*/
@Component
@Slf4j
public class PrototypeBeanMemoryAnalyzer {
@Autowired
private ApplicationContext context;
/**
* 演示prototype Bean不会被Spring自动销毁
* 每次getBean都创建新对象,如果持有大量引用,会导致内存泄漏
*/
public void demonstrateMemoryLeakRisk() {
Runtime runtime = Runtime.getRuntime();
log.info("Before creating prototype beans - Used memory: {}MB",
(runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024);
// 如果把prototype Bean存储在List中而不释放,会导致内存无法回收
List<OrderProcessor> retainedProcessors = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
OrderProcessor processor = context.getBean(OrderProcessor.class);
// 如果这里把processor加入到某个长生命周期的集合中,就会内存泄漏
// retainedProcessors.add(processor); // 这行是内存泄漏的根源
// 正确做法:使用完就让它变成垃圾,等待GC回收
processor.setOrder(createSampleOrder(i));
processor.process();
// processor使用完毕,方法栈帧结束后,processor引用消失,对象可被GC
}
System.gc();
log.info("After creating 1000 prototype beans - Used memory: {}MB",
(runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024);
}
private Order createSampleOrder(int index) {
return Order.builder()
.id("ORD-" + index)
.userId(100L + index)
.amount(new BigDecimal("100.00"))
.build();
}
}五、与相关模式的对比与选型
原型模式 vs 工厂方法模式
- 工厂方法:每次都创建新对象,适合创建逻辑复杂但对象轻量的场景。
- 原型模式:通过克隆现有对象,适合创建成本高、对象初始状态复杂的场景。
原型模式 vs 对象池模式
- 原型:克隆后的对象完全独立,互不影响,使用完后不归还。
- 对象池:对象被复用,使用完后归还池中,适合创建销毁代价极高的对象(如数据库连接)。
六、踩坑实录
坑一:String 类型的"深浅拷贝"误解
很多人会担心 String 类型的字段在浅拷贝中的问题,其实大可不必。String 在 Java 中是不可变的(immutable),任何对 String 的"修改"实际上都是创建新的 String 对象。所以即使浅拷贝让两个对象共享同一个 String 引用,修改其中一个也不会影响另一个。
真正需要注意的是 StringBuilder、List、Map、自定义可变对象这类可变引用类型。
坑二:clone() 中的 final 字段
如果对象中有 final 字段引用了可变对象,在 clone() 中无法重新赋值(final 字段在初始化后不可变),这就导致克隆的对象和原型共享这个 final 字段引用的对象,产生浅拷贝问题。
解决方案:不要在 Cloneable 的类中使用 final 字段引用可变对象,或者使用拷贝构造函数代替 clone()。
坑三:Spring prototype Bean 中包含有状态的 singleton 依赖
一个 prototype Bean 中注入了 singleton Bean,每次新建的 prototype 实例共享同一个 singleton,如果这个 singleton 有状态(比如它内部有缓存),就会产生状态污染。
这在 Spring 设计中是有意为之的——singleton 的依赖不会随着使用方的 scope 而改变。但在某些场景下,需要特别注意这一点,确保 singleton 依赖是真正的无状态或线程安全的。
坑四:克隆后的对象ID处理
克隆出来的对象通常需要一个新的唯一ID(比如数据库主键),而不是延用原型的ID。如果忘记这一点,在持久化克隆对象时会违反主键唯一约束。在克隆方法里要明确地将 ID 字段重置或重新生成:
@Override
public CartTemplate clone() {
CartTemplate cloned = (CartTemplate) super.clone();
cloned.templateId = null; // 克隆出来的模板没有ID,需要新分配
cloned.createdAt = LocalDateTime.now(); // 重置创建时间
// ... 深拷贝其他字段
return cloned;
}七、总结
原型模式看起来简单,但"深拷贝"和"浅拷贝"的边界处理是真正的难点:
- 基本类型和不可变对象(String, Integer, BigDecimal, LocalDateTime):浅拷贝是安全的,无需额外处理。
- 可变集合(List, Map, Set):必须深拷贝,否则修改克隆对象会影响原型。
- 嵌套可变对象:需要递归深拷贝到所有叶子节点,最安全的做法是序列化方式。
- Spring prototype Bean:每次从容器获取都是新实例,但在单例 Bean 中必须通过
Provider<T>或@Lookup来正确使用,直接@Autowired会退化为单例。 - 原型 Bean 的生命周期:Spring 不管理 prototype Bean 的销毁,如果 Bean 持有资源(数据库连接、文件句柄),需要手动调用清理方法,否则会有资源泄漏。
