Spring注入原型Bean,为啥”新“对象“不翼而飞”?

在 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 会通过动态代理自动实现这个方法,每次调用该方法都会返回新的原型实例。 使用步骤超简单:

  1. 单例 Bean 改为抽象类(或普通类,方法为抽象);
  2. 定义抽象方法,返回原型 Bean 类型,添加@Lookup注解;
  3. 每次调用该方法,即可获取新的原型实例。

代码示例:

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()方法获取新实例。

核心原理: ApplicationContextgetBean()方法,对于原型 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 之后,提供了ObjectFactoryProvider两个轻量级接口,专门用于延迟获取 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 真正实现 "每次获取都新" 的特性。

相关推荐
灵感菇_27 分钟前
Java 锁机制全面解析
java·开发语言
indexsunny27 分钟前
互联网大厂Java面试实战:Spring Boot微服务在电商场景中的应用与挑战
java·spring boot·redis·微服务·kafka·spring security·电商
娇娇乔木40 分钟前
模块十一--接口/抽象方法/多态--尚硅谷Javase笔记总结
java·开发语言
saber_andlibert1 小时前
TCMalloc底层实现
java·前端·网络
wangjialelele1 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先
m0_481147331 小时前
拦截器跟过滤器的区别?拦截器需要注册吗?过滤器需要注册吗?
java
Coder_Boy_1 小时前
基于SpringAI的在线考试系统-相关技术栈(分布式场景下事件机制)
java·spring boot·分布式·ddd
独自破碎E1 小时前
【BISHI15】小红的夹吃棋
android·java·开发语言
冻感糕人~1 小时前
【珍藏必备】ReAct框架实战指南:从零开始构建AI智能体,让大模型学会思考与行动
java·前端·人工智能·react.js·大模型·就业·大模型学习
啦啦啦_99991 小时前
Redis实例-2
java