1. 概述
本文将探讨在Spring框架中向单例实例注入原型Bean的几种实现方式。我们将分析每种方案的使用场景及其优缺点。
Spring默认将Bean创建为单例模式。当需要混合不同作用域的Bean时(例如将原型Bean注入单例Bean),就会产生问题——这就是著名的作用域Bean注入问题。
关于Bean作用域的基础知识,建议参考Spring Bean作用域详解。
2. 原型Bean注入问题
先通过配置两个Bean来描述这个问题:
@Configuration
public class AppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
@Bean
public SingletonBean singletonBean() {
return new SingletonBean();
}
}
注意:第一个Bean是原型作用域,第二个是单例作用域。
现在将原型Bean注入单例Bean,并通过getPrototypeBean()
方法暴露:
public class SingletonBean {
// ..
@Autowired
private PrototypeBean prototypeBean;
public SingletonBean() {
logger.info("Singleton instance created");
}
public PrototypeBean getPrototypeBean() {
logger.info(String.valueOf(LocalTime.now()));
return prototypeBean;
}
}
测试代码:加载ApplicationContext
并获取两次单例Bean:
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);
SingletonBean firstSingleton = context.getBean(SingletonBean.class);
PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();
// 再次获取单例Bean实例
SingletonBean secondSingleton = context.getBean(SingletonBean.class);
PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();
isTrue(firstPrototype.equals(secondPrototype), "应该返回相同实例");
}
控制台输出:
Singleton Bean created
Prototype Bean created
11:06:57.894
// 此处应该创建新的原型Bean实例
11:06:58.895
两个Bean都只在应用上下文启动时初始化了一次——这就是问题所在。
3. 注入ApplicationContext
可以直接将ApplicationContext
注入到Bean中:
实现方式有两种:使用@Autowire
注解或实现ApplicationContextAware
接口:
public class SingletonAppContextBean implements ApplicationContextAware {
private ApplicationContext applicationContext;
public PrototypeBean getPrototypeBean() {
return applicationContext.getBean(PrototypeBean.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}
每次调用getPrototypeBean()
方法时,都会从ApplicationContext
返回一个新的PrototypeBean
实例。
但这种方案存在严重缺陷:
- ❌ 违背了控制反转原则(直接从容器获取依赖)
- ❌ 将代码与Spring框架强耦合
4. 方法注入
使用@Lookup
注解实现方法注入是另一种解决方案:
@Component
public class SingletonLookupBean {
@Lookup
public PrototypeBean getPrototypeBean() {
return null;
}
}
Spring会重写被@Lookup
注解的方法,将其注册到应用上下文。每次调用getPrototypeBean()
时都会返回新的PrototypeBean
实例。
底层实现:
- ✅ 使用CGLIB生成字节码
- ✅ 从应用上下文动态获取原型Bean
5. javax.inject API
依赖配置参考Spring依赖注入。
单例Bean实现:
public class SingletonProviderBean {
@Autowired
private Provider<PrototypeBean> myPrototypeBeanProvider;
public PrototypeBean getPrototypeInstance() {
return myPrototypeBeanProvider.get();
}
}
使用Provider
接口注入原型Bean。每次调用getPrototypeInstance()
时,myPrototypeBeanProvider.get()
都会返回新的PrototypeBean
实例。
6. 作用域代理
核心思想: 创建代理对象来连接实际对象和依赖对象,而非直接引用真实对象。
每次调用代理对象的方法时,代理会自行决定创建新实例还是复用现有实例。
修改AppConfig
配置:
@Scope(
value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.TARGET_CLASS)
代理模式说明:
- 默认使用CGLIB直接子类化对象
- 可设置
proxyMode = ScopedProxyMode.INTERFACES
改用JDK动态代理(避免CGLIB)
7. ObjectFactory接口
Spring提供的ObjectFactory<T>
接口可按需生成指定类型对象:
public class SingletonObjectFactoryBean {
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;
public PrototypeBean getPrototypeInstance() {
return prototypeBeanObjectFactory.getObject();
}
}
关键特性:
- ✅ 每次调用
getObject()
返回全新的PrototypeBean
实例 - ✅ 对原型Bean初始化有更多控制权
- ✅ 属于框架原生接口,无需额外配置
8. 使用java.util.Function运行时创建Bean
此方案支持在运行时创建原型Bean实例,并允许添加参数。
先给PrototypeBean
添加name字段:
public class PrototypeBean {
private String name;
public PrototypeBean(String name) {
this.name = name;
logger.info("Prototype instance " + name + " created");
}
//...
}
通过java.util.Function
接口注入Bean工厂:
public class SingletonFunctionBean {
@Autowired
private Function<String, PrototypeBean> beanFactory;
public PrototypeBean getPrototypeInstance(String name) {
PrototypeBean bean = beanFactory.apply(name);
return bean;
}
}
配置类中定义工厂Bean:
@Configuration
public class AppConfig {
@Bean
public Function<String, PrototypeBean> beanFactory() {
return name -> prototypeBeanWithParam(name);
}
@Bean
@Scope(value = "prototype")
public PrototypeBean prototypeBeanWithParam(String name) {
return new PrototypeBean(name);
}
@Bean
public SingletonFunctionBean singletonFunctionBean() {
return new SingletonFunctionBean();
}
//...
}
9. 测试验证
以ObjectFactory
方案为例编写JUnit测试:
@Test
public void givenPrototypeInjection_WhenObjectFactory_ThenNewInstanceReturn() {
AbstractApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);
SingletonObjectFactoryBean firstContext
= context.getBean(SingletonObjectFactoryBean.class);
SingletonObjectFactoryBean secondContext
= context.getBean(SingletonObjectFactoryBean.class);
PrototypeBean firstInstance = firstContext.getPrototypeInstance();
PrototypeBean secondInstance = secondContext.getPrototypeInstance();
assertTrue("应返回新实例", firstInstance != secondInstance);
}
测试结果验证:每次调用getPrototypeInstance()
都会创建新的原型Bean实例。
10. 总结
本文介绍了在Spring中向单例实例注入原型Bean的七种方案:
方案 | 优点 | 缺点 |
---|---|---|
ApplicationContext注入 | 实现简单 | ❌ 违背IoC原则,强耦合 |
@Lookup方法注入 | 代码简洁 | 需CGLIB支持 |
javax.inject API | 标准JSR-330 | 需额外依赖 |
作用域代理 | 配置简单 | 代理可能影响性能 |
ObjectFactory接口 | 框架原生支持 | 需理解工厂模式 |
Function动态创建 | 支持参数化 | 配置稍复杂 |
推荐选择:
- ✅ 简单场景:
@Lookup
方法注入 - ✅ 需参数化:
java.util.Function
方案 - ⚠️ 避免使用:直接注入
ApplicationContext
完整代码示例见GitHub项目。