原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式之一。
原型模式
实际使用就像是Java的克隆clone,以及spring的@Scope("prototype")。
实际使用中,比如异步导出。逻辑都很相似:查询数据、导出数据。
然后可以写一个AsyncExportTask,类上面使用@Scope("prototype")注解。使用时通过ApplicationContext来获取不同的原型bean即可。然后使用异步线程执行即可。
最典型的就是 "原型作用域(Prototype Scope)" 的 Bean 管理。下面用通俗的话讲清楚它在 Spring 中的具体用法、场景和原理:
Spring 中默认的 Bean 是 "单例(Singleton)" 的 ------ 容器启动时创建一次,之后所有地方用的都是同一个对象。但有些场景下,我们需要 "每次获取都新建一个对象",而且这些对象的初始化逻辑相同(比如都需要加载相同的基础配置),这时候就可以用 原型作用域,本质就是 Spring 帮我们实现了 "克隆式创建"。
举个例子:
假设我们有一个 UserService,里面需要初始化一些基础配置(比如数据库连接信息、日志工具),但每次调用时都需要一个新的实例(比如避免多线程共享状态导致的问题)。
定义原型 Bean:用 @Scope("prototype") 标注,告诉 Spring:"这个 Bean 是原型的,每次获取都新创建一个"。
@Service
@Scope("prototype") // 声明为原型作用域
public class UserService {
private String dbUrl;
private Logger logger;
// 初始化方法:加载基础配置(只在创建时执行一次)
@PostConstruct
public void init() {
this.dbUrl = "jdbc:mysql://localhost:3306/test"; // 基础配置
this.logger = LoggerFactory.getLogger(UserService.class);
System.out.println("初始化基础配置完成");
}
public void doSomething() {
logger.info("执行操作,使用数据库:" + dbUrl);
}
}
使用原型 Bean:每次从 Spring 容器中获取 UserService 时,都会得到一个 新的实例,但每个实例的 dbUrl、logger 等基础配置都是通过 init 方法初始化好的(相当于克隆了 "基础配置模板")。
@Component
public class UserController {
@Autowired
private ApplicationContext context;
public void test() {
// 第一次获取:新建一个 UserService 实例,执行 init
UserService service1 = context.getBean(UserService.class);
// 第二次获取:再新建一个 UserService 实例,执行 init
UserService service2 = context.getBean(UserService.class);
// service1 和 service2 是不同的对象,但基础配置相同
System.out.println(service1 == service2); // 输出:false
}
}
二、为什么这是原型模式?
原型模式的核心是 "基于已有模板克隆新对象",而 Spring 原型作用域的逻辑完全符合:
"模板":就是 UserService 的类定义 + @PostConstruct 初始化的基础配置(相当于原型对象)。
"克隆":Spring 每次获取原型 Bean 时,都会通过反射创建一个新实例,并执行初始化方法(相当于 "克隆" 模板的基础配置),避免了手动重复写初始化代码。
"灵活微调":如果需要对新对象做个性化修改(比如给某个 UserService 实例改 dbUrl),直接修改新对象即可,不影响其他实例和 "模板"。
三、适合用原型作用域的场景(原型模式思想)
- 多线程场景:避免状态共享如果 Bean 里有 "状态"(比如成员变量存储临时数据),单例模式下多线程并发访问可能导致数据混乱。用原型模式(原型作用域),每个线程拿到自己的实例,互不干扰。比如:OrderProcessor 处理订单时需要临时存储订单 ID,多线程同时处理时,每个线程用自己的 OrderProcessor 实例,避免订单 ID 串错。
- 对象创建成本高:复用初始化逻辑如果 Bean 的初始化步骤复杂(比如加载配置文件、连接数据库、初始化缓存),每次手动 new 对象并重复初始化代码很麻烦。用原型作用域,Spring 会自动执行初始化逻辑,新对象直接复用这些基础配置。比如:ReportGenerator 需要加载报表模板、设置字体样式等初始化操作,原型模式让每次创建的实例都自带这些配置,不用重复写。
- 临时对象:用完即弃有些对象只是临时用一次(比如处理单次请求的上下文对象 RequestContext),不需要单例复用,用原型模式创建更合适,避免单例占用内存。
四、Spring 原型模式的注意点
和单例 Bean 搭配时要小心如果单例 Bean 依赖了原型 Bean(比如单例 UserController 里注入原型 UserService),默认情况下,Spring 只会在单例初始化时获取一次原型 Bean,之后一直用同一个,不会自动刷新。
解决办法:每次需要时通过 ApplicationContext 手动获取原型 Bean,或者用 ObjectProvider 延迟获取。
原型 Bean 的生命周期自己管,单例 Bean 由 Spring 全程管理(从创建到销毁),但原型 Bean 一旦被创建并交给用户,Spring 就不再管了(不会自动调用 @PreDestroy 销毁方法),需要自己处理资源释放。
原型对比工厂
若新对象和某个已有对象高度相似,只是微调细节,且创建这个对象很麻烦 ------ 用原型模式(复制比新建省事);
若要创建的对象有多种类型,每种类型有固定的创建规则,但没有现成的 "模板对象"------ 用工厂模式(按规则造比维护多个原型省心)。
原型对比享元
原型侧重高效创建复杂对象,减少重复的初始化代码------多个相似复杂对象
享元侧重复用对象减少实例数量,降低内存占用------大量相似的简单对象