在 Spring 开发中,Bean 的作用域是绕不开的核心知识点 ------ 单例 Bean(Singleton)默认全局唯一,原型 Bean(Prototype)则要求每次获取都生成新实例。但很多开发者踩过这样的坑:明明给 Bean 标记了@Scope("prototype"),可注入到单例 Bean 中后,却始终用的是同一个原型实例,所谓的 "原型" 完全名不副实。
这不是 Spring 的 BUG,而是单例 Bean 的初始化机制埋下的 "陷阱"。今天我们就把这个坑的来龙去脉讲透,再给出 3 种简单又实用的解决方法,帮你彻底避开 "原型 Bean 单例化" 的坑。
一、先搞懂:为啥原型 Bean 会被 "单例化"?
要理解这个问题,得先理清 Spring 创建单例 Bean 的核心流程:
单例 Bean 的生命周期是 "一次初始化,全程复用"------Spring 容器启动时(或首次获取单例 Bean 时),会完成该 Bean 的实例化、属性填充(依赖注入)、初始化回调(如@PostConstruct)等全部流程,之后这个 Bean 就被缓存起来,整个应用生命周期内都不会再重新创建。
而依赖注入恰恰发生在 "属性填充" 这一步:
当单例 Bean 依赖一个原型 Bean 时,Spring 只会在初始化单例 Bean 的那一刻,去创建一次原型 Bean 实例并注入进去。后续无论你多少次调用单例 Bean 的方法,用到的都是这同一个原型实例 ------ 原型 Bean 本该 "每次获取都新" 的特性,被单例 Bean 的 "一次性注入" 给覆盖了,这就是 "原型 Bean 单例化" 的本质。
举个直观的例子:
java
// 原型Bean
@Component
@Scope("prototype")
public class PrototypeBean {
private int num = new Random().nextInt(100);
// getter...
}
// 单例Bean(默认单例)
@Component
public class SingletonBean {
@Autowired
private PrototypeBean prototypeBean;
public void printNum() {
System.out.println(prototypeBean.getNum());
}
}
当你多次调用singletonBean.printNum()时,打印的数字始终不变 ------ 哪怕PrototypeBean标记了原型作用域,也只被创建了一次。
二、3 种方案:让原型 Bean 真正 "每次都新"
知道了问题根源,解决思路就很清晰:绕过单例 Bean 的 "一次性注入",每次使用原型 Bean 时,主动去获取新实例。以下是 3 种常用方案,从简单到优雅逐一拆解:
方案 1:@Lookup 注解 ------ 最简洁的声明式方案
@Lookup是 Spring 专门为解决 "单例 Bean 获取原型 Bean" 设计的注解,核心逻辑是:在单例 Bean 中定义一个抽象方法,返回原型 Bean 类型,Spring 会通过动态代理自动实现这个方法,每次调用该方法都会返回新的原型实例。 使用步骤超简单:
- 单例 Bean 改为抽象类(或普通类,方法为抽象);
- 定义抽象方法,返回原型 Bean 类型,添加
@Lookup注解; - 每次调用该方法,即可获取新的原型实例。
代码示例:
java
@Component
public abstract class SingletonBean {
// 核心:@Lookup注解标记抽象方法
@Lookup
public abstract PrototypeBean getPrototypeBean();
public void printNum() {
// 每次调用getPrototypeBean(),都是新实例
System.out.println(getPrototypeBean().getNum());
}
}
优点 :代码极简,完全贴合 Spring 的声明式编程风格,无需手动操作容器; 缺点:单例 Bean 必须是抽象类(或方法为抽象),对代码结构有轻微侵入。
方案 2:实现 ApplicationContextAware------ 手动获取容器
如果不想用抽象类,可通过实现ApplicationContextAware接口,让单例 Bean 持有 Spring 上下文对象,每次需要原型 Bean 时,手动调用applicationContext.getBean()方法获取新实例。
核心原理: ApplicationContext的getBean()方法,对于原型 Bean,每次调用都会创建新实例;而单例 Bean 的getBean()则返回缓存实例。
代码示例:
java
@Component
public class SingletonBean implements ApplicationContextAware {
// 持有Spring上下文对象
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void printNum() {
// 手动获取原型Bean,每次都是新实例
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
System.out.println(prototypeBean.getNum());
}
}
优点 :灵活性高,无需修改类的抽象性,适合需要自定义获取逻辑的场景; 缺点 :直接耦合ApplicationContext,违背 "控制反转" 的设计思想,代码侵入性稍强。
方案 3:ObjectFactory/Provider------Spring 4 + 推荐的优雅方案
Spring 4 之后,提供了ObjectFactory和Provider两个轻量级接口,专门用于延迟获取 Bean 实例,是解决该问题的 "最优解"------ 既不耦合容器,也无需抽象类,兼顾简洁与优雅。
用法 1:ObjectFactory(Spring 内置)
注入ObjectFactory<PrototypeBean>,每次调用getObject()方法获取新实例:
java
@Component
public class SingletonBean {
// 注入ObjectFactory,泛型指定原型Bean类型
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanFactory;
public void printNum() {
// 每次getObject(),返回新的原型实例
PrototypeBean prototypeBean = prototypeBeanFactory.getObject();
System.out.println(prototypeBean.getNum());
}
}
用法 2:Provider(JSR-330 标准)
Provider是 JSR-330 规范的接口,Spring 对其提供了默认实现,用法与ObjectFactory几乎一致,更符合跨框架的标准:
java
@Component
public class SingletonBean {
// 注入Provider,需导入javax.inject.Provider(需引入依赖)
@Autowired
private javax.inject.Provider<PrototypeBean> prototypeBeanProvider;
public void printNum() {
// 每次get(),返回新的原型实例
PrototypeBean prototypeBean = prototypeBeanProvider.get();
System.out.println(prototypeBean.getNum());
}
}
注意:使用Provider需要引入 JSR-330 依赖(Maven):
xml
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
优点:
- 完全解耦
ApplicationContext,符合 Spring 的设计理念; - 无需抽象类,代码侵入性极低;
ObjectFactory无需额外依赖,Provider符合标准,适配性更强;
缺点:需 Spring 4 及以上版本(主流项目均满足)。
三、方案对比与选型建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| @Lookup 注解 | 代码极简,声明式风格 | 需抽象类 / 抽象方法 | 追求简洁,接受轻微代码结构调整 |
| ApplicationContextAware | 灵活性高,无抽象限制 | 耦合容器,侵入性强 | 需自定义获取逻辑,兼容低版本 Spring |
| ObjectFactory/Provider | 优雅解耦,无侵入,Spring 推荐 | 需 Spring 4+(Provider 需额外依赖) | 主流场景,优先推荐 |
四、总结
Spring 单例 Bean 注入原型 Bean 时的 "原型单例化" 坑,本质是单例 Bean "一次性注入" 的生命周期特性导致的。解决核心是放弃 "一次性注入",改为 "按需获取":
- 追求极简选
@Lookup; - 兼容低版本或需自定义逻辑选
ApplicationContextAware; - 主流场景优先选
ObjectFactory/Provider(Spring 4+)。
掌握这 3 种方案,就能彻底避开这个高频坑,让原型 Bean 真正实现 "每次获取都新" 的特性。