原型模式实战:Java深克隆与Spring原型Bean应用指南 📅 发布时间:2026/9/1 10:55:46 👁 浏览次数: 大家好我是专注于分享技术实战经验的博主。在软件开发中你是否遇到过这样的场景创建一个复杂对象比如包含大量配置、嵌套对象或数据库连接的对象的成本非常高而你的程序又需要大量相似但又不完全相同的对象实例。如果每次都通过new关键字调用构造函数不仅性能堪忧代码也会变得臃肿。今天我们就来深入探讨一个能优雅解决此问题的设计模式——原型模式Prototype Pattern。本文将从核心概念、UML图解、代码实现、应用场景到与Spring框架的结合为你提供一份从入门到精通的完整指南无论你是正在完成设计模式大作业的学生还是寻求项目优化的开发者都能从中获得可直接复用的实战方案。1. 原型模式的核心概念与价值1.1 什么是原型模式原型模式是一种创建型设计模式。它的核心思想非常简单用原型实例指定创建对象的种类并且通过拷贝这个原型来创建新的对象。你可以把它想象成细胞的“有丝分裂”。我们不需要每次都从零开始“合成”一个细胞而是让一个已经存在的、功能完备的细胞原型复制自身从而快速产生一个基因相同的新细胞。在软件中这个“复制”的过程我们通常称之为“克隆Clone”。1.2 它解决了什么问题原型模式主要为了解决以下两类问题创建成本高当一个对象的初始化过程非常复杂需要从数据库、文件、网络读取大量数据或者需要进行繁重的计算时直接new一个对象的开销很大。避免构造函数的约束有些对象的创建可能依赖于运行时环境或动态配置通过构造函数难以灵活地构建出各种变体。而克隆一个已配置好的原型则可以绕过构造函数的限制。1.3 为什么需要掌握它掌握原型模式意味着你掌握了另一种对象创建的“武器”。它不仅仅是new的替代品更是一种设计思想的体现——通过复制已有对象来快速构建新对象。在以下场景中它能显著提升代码的效率和灵活性游戏开发快速生成大量属性相似但位置、状态不同的怪物、子弹。配置对象系统启动时加载一个复杂的配置原型后续不同模块使用时克隆并微调。文档编辑需要创建与现有文档格式、样式完全一致的新文档。缓存对象将耗资源创建的对象作为原型放入缓存后续请求直接克隆使用。2. 原型模式的原理与UML类图理解一个设计模式看图是最直观的方式。下面我们来看原型模式的经典UML类图。--------------------- | interface | | Prototype | --------------------- | clone(): Prototype| --------------------- ^ | 实现 | --------------------- | ConcretePrototype | --------------------- | - field: Type | --------------------- | clone(): Prototype| ---------------------角色解析Prototype抽象原型接口声明一个克隆自身的接口通常就是clone()方法。这是所有具体原型类的父类。ConcretePrototype具体原型类实现Prototype接口实现具体的克隆操作。它是被复制的对象。Client客户端使用原型对象的客户端。它通过请求原型对象克隆自身来创建新对象而不是直接new。核心流程客户端需要一个新对象。客户端找到或持有一个符合要求的原型对象ConcretePrototype实例。客户端调用原型对象的clone()方法。clone()方法内部创建当前对象的一个副本并返回。客户端获得新对象其状态与原型对象初始时相同。3. 环境准备与版本说明本文将使用Java语言进行代码演示因为其内置的Cloneable接口和Object.clone()方法与原型模式天然契合。同时我们也会探讨在Spring Framework中原型模式的应用。JDK版本建议使用 JDK 8 或以上版本。本文示例在 JDK 11 环境下测试通过。IDEIntelliJ IDEA, Eclipse, VS Code 等均可。构建工具Maven 或 Gradle用于Spring示例部分。Spring Boot版本在讨论Spring应用时我们以 Spring Boot 2.7.x 为例。示例项目结构预览prototype-pattern-demo/ ├── src/main/java/com/example/prototype/ │ ├── simple/ (浅克隆与深克隆示例) │ ├── registry/ (原型管理器示例) │ └── spring/ (Spring中的原型模式) ├── pom.xml (Maven依赖) └── README.md4. 核心实现浅克隆与深克隆这是实现原型模式时最核心、也最容易踩坑的部分。Java 提供了Cloneable标记接口和Object.clone()方法但默认实现是浅克隆Shallow Clone。4.1 浅克隆Shallow Clone浅克隆只复制对象本身以及其基本数据类型字段和String不可变对象字段。对于对象内部的引用类型字段它只复制引用地址新旧对象共享同一个子对象。示例一个简单的简历对象// 文件路径src/main/java/com/example/prototype/simple/Resume.java import java.util.Date; public class Resume implements Cloneable { private String name; private Date birthday; // 引用类型字段 private String school; public Resume(String name, Date birthday, String school) { this.name name; this.birthday birthday; this.school school; } // Getter and Setter 省略... // 重写 clone 方法实现浅克隆 Override public Resume clone() { try { // 调用 Object 的 native 方法进行浅拷贝 return (Resume) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // 因为实现了 Cloneable不会发生 } } Override public String toString() { return Resume{ name name \ , birthday birthday , school school \ }; } }测试浅克隆的问题// 文件路径src/main/java/com/example/prototype/simple/ShallowCloneTest.java import java.util.Date; public class ShallowCloneTest { public static void main(String[] args) { Date birthday new Date(94, 0, 1); // 1994-01-01 Resume resumeA new Resume(张三, birthday, 清华大学); // 克隆一份简历 Resume resumeB resumeA.clone(); resumeB.setName(李四); // 修改克隆体的名字 // 修改原型对象中的 Date 对象 resumeA.getBirthday().setYear(122); // 改为 2022年 System.out.println(原型 ResumeA: resumeA); System.out.println(克隆体 ResumeB: resumeB); System.out.println(birthday 是同一个对象吗 (resumeA.getBirthday() resumeB.getBirthday())); } }运行结果原型 ResumeA: Resume{name张三, birthdaySat Jan 01 00:00:00 CST 2022, school清华大学} 克隆体 ResumeB: Resume{name李四, birthdaySat Jan 01 00:00:00 CST 2022, school清华大学} birthday 是同一个对象吗 true可以看到虽然name字段独立了但birthday这个Date对象被resumeA和resumeB共享了。修改其中一个另一个也跟着变。这通常不是我们想要的效果。4.2 深克隆Deep Clone深克隆要求复制对象本身以及其所有引用类型字段指向的对象直到所有可达对象都被复制从而创建一个完全独立的副本。实现深克隆的几种方式方式一重写clone()方法手动复制引用对象// 文件路径src/main/java/com/example/prototype/simple/ResumeDeepClone.java import java.util.Date; public class ResumeDeepClone implements Cloneable { private String name; private Date birthday; private String school; public ResumeDeepClone(String name, Date birthday, String school) { this.name name; this.birthday birthday; this.school school; } // Getter and Setter 省略... Override public ResumeDeepClone clone() { try { ResumeDeepClone clone (ResumeDeepClone) super.clone(); // 浅克隆基础 // 对引用类型字段进行深拷贝 if (this.birthday ! null) { clone.birthday (Date) this.birthday.clone(); // Date 也实现了 Cloneable } // 如果 school 是可变对象也需要深拷贝但 String 不可变所以不用 return clone; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }方式二通过序列化实现深克隆推荐适用于复杂对象图这种方式不要求嵌套对象实现Cloneable但要求整个对象图都是可序列化的实现Serializable接口。// 文件路径src/main/java/com/example/prototype/simple/DeepCloneBySerialization.java import java.io.*; public class DeepCloneBySerialization { SuppressWarnings(unchecked) public static T extends Serializable T deepClone(T obj) { T cloneObj null; // 将对象写入字节流 try (ByteArrayOutputStream baos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(baos)) { oos.writeObject(obj); oos.flush(); // 从字节流中读出对象 try (ByteArrayInputStream bais new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois new ObjectInputStream(bais)) { cloneObj (T) ois.readObject(); } } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } return cloneObj; } } // 使用示例 class ComplexObject implements Serializable { private ListString dataList; private MapString, Object configMap; // ... 其他字段也都需要实现 Serializable // Getter/Setter 省略 } // 在客户端调用 ComplexObject proto new ComplexObject(); // ... 设置 proto 的状态 ComplexObject copy DeepCloneBySerialization.deepClone(proto); // 获得一个深克隆副本方式三使用第三方库如 Apache Commons Lang3, Jackson, Gson这些库提供了更简便的深拷贝工具方法。// 使用 Apache Commons Lang3 (需要引入依赖) import org.apache.commons.lang3.SerializationUtils; // 要求对象实现 Serializable ComplexObject copy SerializationUtils.clone(proto); // 使用 Jackson (需要引入 jackson-databind) import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper mapper new ObjectMapper(); ComplexObject copy mapper.readValue(mapper.writeValueAsString(proto), ComplexObject.class);如何选择对象结构简单且所有引用对象都实现了Cloneable方式一手动clone。对象结构复杂或不想让所有类都实现Cloneable方式二序列化或方式三第三方库。序列化方式通用性强但性能有损耗Jackson/Gson 方式灵活但依赖特定库。5. 进阶实战原型管理器Prototype Registry在实际项目中我们通常会有多种类型的原型对象。直接在各处维护这些原型实例并不优雅。原型管理器模式应运而生它集中管理所有可克隆的原型对象客户端只需通过一个标识符如Key就能获取对应的原型并进行克隆。场景一个图形编辑器可以创建多种预定义形状圆形、矩形、三角形。实现// 文件路径src/main/java/com/example/prototype/registry/Graphic.java // 1. 抽象原型 public abstract class Graphic implements Cloneable { protected String type; protected String color; abstract void draw(); public String getType() { return type; } public void setColor(String color) { this.color color; } Override public Graphic clone() { Graphic clone null; try { clone (Graphic) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } } // 2. 具体原型 - 圆形 class Circle extends Graphic { private int radius; public Circle() { this.type Circle; this.color Red; this.radius 10; } Override public Circle clone() { Circle clone (Circle) super.clone(); // 如果有引用类型字段这里需要深拷贝 return clone; } Override void draw() { System.out.println(Drawing a color type with radius radius); } public void setRadius(int radius) { this.radius radius; } } // 3. 具体原型 - 矩形 class Rectangle extends Graphic { private int width; private int height; public Rectangle() { this.type Rectangle; this.color Blue; this.width 20; this.height 15; } // 省略 clone(), draw(), setter 等方法结构同 Circle } // 4. 原型管理器 import java.util.HashMap; import java.util.Map; public class GraphicPrototypeRegistry { private MapString, Graphic prototypeMap new HashMap(); public GraphicPrototypeRegistry() { loadPrototypes(); } private void loadPrototypes() { // 初始化时创建并注册各种原型 Circle circle new Circle(); Rectangle rectangle new Rectangle(); prototypeMap.put(CIRCLE, circle); prototypeMap.put(RECTANGLE, rectangle); // 可以注册更多... } // 客户端通过Key获取原型并克隆 public Graphic getGraphic(String type) { Graphic prototype prototypeMap.get(type); if (prototype ! null) { return prototype.clone(); // 关键返回克隆体而非原型本身 } return null; } // 动态注册新原型可选 public void addPrototype(String key, Graphic prototype) { prototypeMap.put(key, prototype); } } // 5. 客户端使用 public class Client { public static void main(String[] args) { GraphicPrototypeRegistry registry new GraphicPrototypeRegistry(); // 获取一个红色圆形原型并克隆它 Graphic circle1 registry.getGraphic(CIRCLE); circle1.draw(); // Drawing a Red Circle with radius 10 // 克隆另一个圆形并修改其属性 Graphic circle2 registry.getGraphic(CIRCLE); circle2.setColor(Green); ((Circle) circle2).setRadius(15); circle2.draw(); // Drawing a Green Circle with radius 15 // 获取矩形 Graphic rectangle registry.getGraphic(RECTANGLE); rectangle.draw(); // Drawing a Blue Rectangle with width 20 and height 15 } }优势解耦客户端与具体原型类解耦只依赖管理器和抽象接口。易于扩展新增一种图形只需创建新的ConcretePrototype并在管理器中注册无需修改客户端代码。性能优化原型对象只在管理器初始化时创建一次后续都是克隆操作避免了重复的昂贵初始化。6. 原型模式在Spring框架中的应用Spring Framework 广泛使用了原型模式的思想最典型的体现就是Bean 的作用域Scope。6.1 Spring 中的原型作用域在 Spring 的 IoC 容器中默认的 Bean 作用域是单例singleton即整个容器中只有一个实例。而原型prototype作用域则表示每次请求通过applicationContext.getBean()或注入都会创建一个新的 Bean 实例。配置方式XML配置bean idmyPrototypeBean classcom.example.MyBean scopeprototype/注解配置Component ScopeComponent Scope(prototype) // 或 Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class MyPrototypeBean { // ... }Java Config配置Bean ScopeConfiguration public class AppConfig { Bean Scope(prototype) public MyBean myBean() { return new MyBean(); } }6.2 Spring 如何实现原型BeanSpring 容器并不直接存储原型Bean的实例。当Bean的作用域被定义为prototype时容器启动时会创建并管理该Bean的定义BeanDefinition。当客户端如另一个Bean或通过ApplicationContext请求该Bean时容器会根据其BeanDefinition每次都调用相应的初始化逻辑如构造函数、PostConstruct方法、InitializingBean接口来创建一个全新的实例然后返回给客户端。容器不负责原型Bean的生命周期管理如销毁回调PreDestroy不会被容器调用。示例// 文件路径src/main/java/com/example/prototype/spring/PrototypeBeanDemo.java import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; Configuration public class PrototypeBeanDemo { static class Task { private static int counter 0; private final int id; public Task() { this.id counter; System.out.println(Task 实例被创建ID: id); } public void execute() { System.out.println(执行任务ID: id); } } Bean Scope(prototype) // 关键声明为原型作用域 public Task prototypeTask() { return new Task(); } public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(PrototypeBeanDemo.class); // 第一次获取 Task task1 context.getBean(Task.class); task1.execute(); // 第二次获取 Task task2 context.getBean(Task.class); task2.execute(); System.out.println(task1 和 task2 是同一个实例吗 (task1 task2)); context.close(); } }输出Task 实例被创建ID: 1 执行任务ID: 1 Task 实例被创建ID: 2 执行任务ID: 2 task1 和 task2 是同一个实例吗 false可以看到每次getBean()都创建了一个新的Task实例。6.3 注意事项与常见问题性能开销原型Bean每次都是新建如果创建过程复杂频繁请求会影响性能。需权衡使用场景。生命周期管理Spring不管理原型Bean的销毁如果原型Bean持有资源如文件句柄、数据库连接需要客户端自己确保正确释放。注入单例Bean中的原型Bean这是一个经典问题。如果你将一个原型Bean注入到一个单例Bean中由于单例Bean只初始化一次所以它持有的原型Bean引用也就固定了后续无法获取新的原型实例。解决方案是使用方法注入Lookup注解。使用ObjectFactory或Provider。每次需要时直接从ApplicationContext中获取不推荐增加耦合。7. 常见问题与排查思路在实际应用原型模式时你可能会遇到以下问题问题现象常见原因解决思路克隆后对象状态意外共享使用了浅克隆但对象包含可变引用类型字段。实现深克隆。检查所有引用类型字段确保它们也被正确克隆或本身就是不可变对象如String,Integer。CloneNotSupportedException类没有实现Cloneable接口却调用了Object.clone()。让类实现Cloneable接口。这是一个标记接口不实现它Object.clone()会抛出此异常。序列化深克隆失败1. 类或其字段未实现Serializable。2. 存在不可序列化的字段如Thread,Socket。3. 序列化ID (serialVersionUID) 不一致。1. 确保整个对象图都实现Serializable。2. 将不可序列化字段标记为transient或在readObject/writeObject方法中自定义序列化逻辑。3. 显式定义private static final long serialVersionUID。Spring原型Bean注入单例Bean后不生效单例Bean在初始化时注入了一次原型Bean之后一直使用这个固定实例。使用Lookup注解、ObjectFactoryT或ProviderT进行延迟查找。克隆性能瓶颈对象结构极其复杂深克隆尤其是序列化方式开销大。1. 考虑是否真的需要深克隆或许浅克隆部分字段手动复制即可。2. 评估使用第三方高性能拷贝工具如 MapStruct, BeanUtils 用于属性复制。3. 使用“原型管理器”缓存原型避免重复创建原型本身。克隆破坏了单例模式如果单例类实现了Cloneable并提供了clone()方法则可能被克隆破坏单例。绝对不要让单例类实现Cloneable接口。如果必须实现则重写clone()方法并直接返回当前实例return this;或抛出CloneNotSupportedException。8. 最佳实践与工程建议优先考虑“复制”而非“新建”当对象的创建成本显著高于复制成本且需要创建多个相似对象时应首先考虑原型模式。明确克隆的深度在实现clone()方法时必须在文档中清晰说明是浅克隆还是深克隆避免团队成员误用。更推荐实现深克隆以避免隐蔽的Bug。使用原型管理器对于系统中存在多种“原型”的情况使用原型管理器进行集中管理提高代码的可维护性和可扩展性。谨慎实现CloneableJava 的Cloneable接口存在设计缺陷没有clone方法声明依赖Object的受保护方法。考虑以下替代方案复制构造函数public MyClass(MyClass other) { ... }复制工厂方法public static MyClass newInstance(MyClass template) { ... }使用第三方库如 Apache Commons Lang3 的SerializationUtils.clone()或使用序列化/反序列化。在Spring中合理使用原型作用域适合场景有状态的Bean每次请求需要独立实例如购物车、用户会话数据封装对象。不适合场景无状态的工具类、服务类、配置类应使用单例。注意资源释放原型Bean如果持有资源需确保有正确的销毁机制如实现DisposableBean接口但需客户端调用销毁方法。性能测试在决定使用深克隆尤其是序列化方式前应对其性能影响进行评估。对于高频操作可能需要寻找更高效的拷贝策略。保持不可变性如果一个类被设计为不可变如String,Integer那么浅克隆就是安全的因为其内部状态无法被改变。在设计可克隆类时尽量使字段为基本类型或不可变对象可以简化克隆逻辑。原型模式是一种强大而灵活的对象创建工具它通过“克隆”来规避复杂的初始化过程。理解其浅克隆与深克隆的本质区别是关键这决定了副本的独立性。在Java中可以借助Cloneable接口、序列化或第三方库来实现。在大型项目中结合“原型管理器”能更好地组织代码。而在Spring生态中原型作用域是这一模式在IoC容器中的直接体现。希望这篇结合原理、代码与实战的文章能帮助你在下次面临“如何高效创建复杂对象”时多一个清晰、有效的解决方案。