大家有没有遇到过"明明配置了Prototype作用域,调用时却始终拿到同一个实例"的情况? 今天我们就从这个常见错误场景入题,一步步学会Prototype Bean的正确使用方式,搞懂每种方式的优劣,再也不踩坑!
一、高频踩坑:单例Bean依赖Prototype Bean
我们先看一个实际开发中最容易写错的代码,大家可以对照自己的项目,看看有没有类似写法:
java
// 1. 定义Prototype Bean(
@Component
@Scope("prototype")
public class PrototypeBean {
private String id;
// 构造方法,每次实例化时生成唯一ID
public PrototypeBean() {
this.id = UUID.randomUUID().toString().substring(0, 8);
}
// getter方法
public String getId() {
return id;
}
}
// 2. 定义Singleton Bean依赖上面的Prototype Bean
@Service
public class SingletonService {
// 直接用@Autowired注入Prototype Bean
@Autowired
private PrototypeBean prototypeBean;
// 获取Prototype Bean的ID
public String getPrototypeBeanId() {
return prototypeBean.getId();
}
}
// 3. 测试代码
@SpringBootTest
public class PrototypeTest {
@Autowired
private SingletonService singletonService;
@Test
public void testErrorUsage() {
// 多次调用,返回不同的ID(新实例)
String id1 = singletonService.getPrototypeBeanId();
String id2 = singletonService.getPrototypeBeanId();
String id3 = singletonService.getPrototypeBeanId();
// 结果:三次ID完全相同,说明只创建了一个Prototype Bean实例
System.out.println("第一次ID:" + id1);
System.out.println("第二次ID:" + id2);
System.out.println("第三次ID:" + id3);
}
}
运行测试后会发现,三次获取的ID完全一致------原因很简单:Singleton Bean是全局单例,初始化时只会注入一次Prototype Bean,后续无论调用多少次,用的都是第一次注入的那个实例,Prototype Bean的"每次新实例"特性完全失效。
既然直接注入会踩坑,那我们该如何正确获取Prototype Bean呢?接下来,我们就逐一讲解四种正确使用方式。
二、Prototype Bean 四种正确使用方式(附代码+优劣)
方式一:通过ApplicationContextAware 编程式获取
核心思路:让服务类实现ApplicationContextAware 接口,获取Spring容器上下文,然后通过applicationContext.getBean()方法,每次主动获取Prototype Bean------因为每次调用getBean(),Spring都会创建一个新的Prototype实例。
java
//ApplicationContextAware类
@Service
public class PrototypeBeanServiceByApplicationAware implements ApplicationContextAware {
// Spring容器上下文
private ApplicationContext applicationContext;
// 实现接口方法,Spring会自动注入ApplicationContext
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public PrototypeBean getPrototypeBean() {
// 每次调用getBean(),都会创建新的Prototype实例
return applicationContext.getBean(PrototypeBean.class);
}
}
// 3. 测试代码
@Test
public void testApplicationContextAware() {
@Autowired
private PrototypeBeanServiceByApplicationAware service;
// 多次调用,观察ID同
String id1 = service.getPrototypeBean().getId();
String id2 = service.getPrototypeBean().getId();
String id3 = service.getPrototypeBean().getId();
System.out.println("第一次ID:" + id1);
System.out.println("第二次ID:" + id2);
System.out.println("第三次ID:" + id3);
}
优点:编程方式,灵活度高,可根据业务逻辑动态获取Prototype Bean,不受注入方式限制。
缺点:代码耦合度高,服务类直接依赖Spring的ApplicationContext,脱离Spring容器后无法独立测试;代码冗长,每个需要的地方都要注入 ApplicationContext,或者创建PrototypeBeanServiceByApplicationAware 这种代理的方式
方式二:通过@Lookup 注解获取
核心思路:使用Spring提供的**@Lookup**注解,Spring会自动重写该方法,每次调用方法时,都会返回一个新的Prototype Bean实例。
java
// 用@Lookup注解的服务类
@Service
public class PrototypeBeanServiceByLookup {
@Lookup
public PrototypeBean getPrototypeBean() {
return null;
}
}
// 3. 测试代码
@Test
public void testLookup() {
@Autowired
private PrototypeBeanServiceByLookup service;
String id1 = service.getPrototypeBean().getId();
String id2 = service.getPrototypeBean().getId();
String id3 = service.getPrototypeBean().getId();
System.out.println("第一次ID:" + id1);
System.out.println("第二次ID:" + id2);
System.out.println("第三次ID:" + id3);
}
补充说明:@Lookup注解也可以用在抽象方法上,spring会自动实现该方法。
优点:相比方式一代码简洁,只需要一个注解和一个方法;
缺点:要写个空方法,让人感觉奇怪
方式三:通过ObjectFactory 获取
核心思路:使用Spring提供的ObjectFactory 接口,它是一个"工厂类",专门用于获取Bean实例,注入ObjectFactory后,每次调用getObject()方法,都会返回一个新的Prototype Bean。
java
// 用ObjectFactory的服务类
@Service
public class PrototypeBeanServiceByObjectFactory {
// 注入ObjectFactory
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;
// 每次调用getObject(),获取新的Prototype实例
public PrototypeBean getPrototypeBean() {
return prototypeBeanObjectFactory.getObject();
}
}
// 3. 测试代码
@Test
public void testObjectFactory() {
@Autowired
private PrototypeBeanServiceByObjectFactory service;
String id1 = service.getPrototypeBean().getId();
String id2 = service.getPrototypeBean().getId();
String id3 = service.getPrototypeBean().getId();
System.out.println("第一次ID:" + id1);
System.out.println("第二次ID:" + id2);
System.out.println("第三次ID:" + id3);
}
补充说明:ObjectFactory是Spring的轻量级工厂接口,仅提供getObject()一个方法,专门用于"延迟获取Bean",适合用于获取Prototype Bean,既解耦,又不会增加过多开销。
优点:相比于@Lookup,不需要再写个方法,直接注入即可
方式四:通过ObjectProvider 获取(推荐)
ObjectProvider是Spring 4.3+新增的接口,是ObjectFactory的增强版,不仅能获取Bean实例,还提供了多种灵活的获取方法(比如判断Bean是否存在、获取唯一Bean等),是目前获取Prototype Bean的推荐方式。
java
// 用ObjectProvider的服务类
@Service
public class PrototypeBeanServiceByObjectProvider {
// 注入ObjectProvider
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
// 获取Prototype实例(和ObjectFactory类似)
public PrototypeBean getPrototypeBean() {
return prototypeBeanObjectProvider.getIfAvailable();
}
}
// 3. 测试代码
@Test
public void testObjectProvider() {
@Autowired
private PrototypeBeanServiceByObjectProvider service;
String id1 = service.getPrototypeBean().getId();
String id2 = service.getPrototypeBean().getId();
String id3 = service.getPrototypeBean().getId();
System.out.println("第一次ID:" + id1);
System.out.println("第二次ID:" + id2);
System.out.println("第三次ID:" + id3);
}
补充说明:getIfUnique()是获取bean的另一种方式,简单说下和getIfAvailable区别:
-
getIfAvailable():"有就拿,没有就算了"(返回null),如果同类型bean有多个,则抛异常;
-
getIfUnique():"有且只有一个",否则返回null,如果同类型bean有多个也返回null。
优点:功能强大,支持多种获取方式,是Spring推荐的方式。
四种方式我们都讲完了,每种方式都有自己的优劣和适用场景。但实际开发中,我们常会遇到同一类型存在多个Prototype Bean的情况(比如通过@Bean定义多个同类型实例),这种场景下该如何精准获取目标Bean呢?接下来我们说下这种特殊场景的处理方法。
四、特殊场景:同一类型存在多个Prototype Bean的处理
场景代码如下:
java
@Configuration
public class Config {
// 第一个Prototype Bean,bean名称默认是方法名prototypeBean
@Bean
@Scope("prototype")
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
// 第二个Prototype Bean,bean名称为prototypeBeanA
@Bean
@Scope("prototype")
public PrototypeBean prototypeBeanA() {
return new PrototypeBean();
}
// 第三个Prototype Bean,bean名称为prototypeBeanB
@Bean
@Scope("prototype")
public PrototypeBean prototypeBeanB() {
return new PrototypeBean();
}
}
此时容器中存在3个PrototypeBean类型的Bean。此时直接用之前的方式获取会出现问题,可能返回null,可能报错,我们针对四种方式,分别给出解决方案:
1. 方式一(ApplicationContextAware):通过getBean指定bean名称
核心思路:在调用applicationContext.getBean()方法时,增加一个参数(bean名称),精准指定要获取的目标Bean,避免歧义。
java
@Service
public class PrototypeBeanServiceByApplicationAware implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
// 分别获取不同名称的Prototype Bean
public PrototypeBean getPrototypeBean() {
// 指定bean名称为prototypeBean
return applicationContext.getBean("prototypeBean", PrototypeBean.class);
}
}
2. 方式二(@Lookup注解):通过@Lookup指定bean名称
核心思路:在@Lookup注解中添加value属性,指定目标Bean的名称。
java
@Service
public class PrototypeBeanServiceByLookup {
// 指定获取名称为prototypeBean的Bean
@Lookup("prototypeBean")
public PrototypeBean getPrototypeBean() {
return null;
}
}
3. 方式三、方式四(ObjectFactory/ObjectProvider):通过字段名匹配bean名称
核心思路:Spring自动注入时,会根据字段名匹配容器中的Bean名称,即可精准获取对应实例。
java
@Service
public class PrototypeBeanServiceByObjectFactory {
// 字段名=bean名称,获取该名称的Bean
@Autowired
private ObjectFactory<PrototypeBean> prototypeBean;
// 获取方法
public PrototypeBean getPrototypeBean() {
return prototypeBean.getObject();
}
}
总结:同一类型存在多个Prototype Bean时,核心是"通过名称定位Bean"。
三、总结:四种方式对比与选择
| 使用方式 | 核心优点 | 核心缺点 |
|---|---|---|
| ApplicationContextAware | 灵活度高,可动态获取 | 耦合Spring容器,测试不便 |
| @Lookup注解 | 代码简洁 | 需要添加一个"鸡肋"方法 |
| ObjectFactory | 直接注入,简便 | 多bean时,如果不处理会抛异常 |
| ObjectProvider | 功能强大,灵活,Spring推荐 | 无 |
总结:日常开发中,优先选择ObjectProvider;如果场景简单,可选择@Lookup或ObjectFactory;只有需要动态Bean获取逻辑时,才考虑ApplicationContextAware。
觉得有用的话,点赞,转发,推荐,支持一下吧!