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 真正实现 "每次获取都新" 的特性。

相关推荐
初听于你1 小时前
Thymeleaf 模板引擎讲解
java·服务器·windows·spring boot·spring·eclipse
刘 大 望1 小时前
JVM(Java虚拟机)
java·开发语言·jvm·数据结构·后端·java-ee
超级种码1 小时前
JVM 字节码指令活用手册(基于 Java 17 SE 规范)
java·jvm·python
元亓亓亓1 小时前
LeetCode热题100--155. 最小栈--中等
java·算法·leetcode
SadSunset1 小时前
(3)第一个spring程序
java·后端·spring
高山上有一只小老虎1 小时前
小红的双生串
java·算法
TDengine (老段)1 小时前
人力减 60%:时序数据库 TDengine 助力桂冠电力实现 AI 智能巡检
java·大数据·数据库·人工智能·时序数据库·tdengine·涛思数据
yaoxin5211232 小时前
263. Java 集合 - 遍历 List 时选用哪种方式?ArrayList vs LinkedList
java·开发语言·list
JH30732 小时前
Redisson vs Jedis vs Lettuce
java·redis