Spring Prototype Bean的四种正确使用方式

大家有没有遇到过"明明配置了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。

觉得有用的话,点赞,转发,推荐,支持一下吧!

相关推荐
代码丰2 小时前
一篇讲透:Spring Boot + Redisson + 注解 + AOP 实现接口限流(可直接落地)
后端
永恒_顺其自然2 小时前
Java Web 传统项目异步分块上传系统实现方案
java·开发语言·前端
赫瑞2 小时前
Java中的大数处理 —— BigInteger
java·开发语言
r_oo_ki_e_2 小时前
java25--Collection集合
java·开发语言
色空大师2 小时前
网站搭建实操(五)后台管理-短信模块
java·阿里云短信·网站·短信
极创信息2 小时前
信创软件安全加固指南,信创软件的纵深防御体系
java·大数据·数据库·金融·php·mvc·软件工程
蜘蛛侠..3 小时前
什么是 Plan-and-Execute 模式?与ReAct模式区别?
java·ai·大模型·llm·agent·react·plan模式
蚂蚁集团分布式架构3 小时前
🦐 不办 Meetup,开挑战赛!SOFAStack PR Challenge | SOFAStack 8 周年
后端·github·claude
untE EADO3 小时前
SpringBoot:几种常用的接口日期格式化方法
java·spring boot·后端