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

相关推荐
Nonoas18 小时前
动态代理:发布订阅的高级玩法
java·ide·intellij-idea
程序员-周李斌19 小时前
Java 死锁
java·开发语言·后端
皮皮林55119 小时前
Prometheus+Grafana,打造强大的监控与可视化平台
java
JasmineWr19 小时前
CompletableFuture相关问题
java·开发语言
零雲19 小时前
java面试:知道java的反射机制吗
java·开发语言·面试
java1234_小锋20 小时前
Java进程占用的内存有哪些部分?
java
sxlishaobin20 小时前
Spring Bean生命周期详解
java·后端·spring
曹牧21 小时前
Java:Assert.isTrue()
java·前端·数据库
梦里小白龙21 小时前
JAVA 策略模式+工厂模式
java·开发语言·策略模式
你不是我我21 小时前
【Java 开发日记】我们来说一说 Redis 主从复制的原理及作用
java·redis·github