spring 的 多例(prototype)作用域 (scope="prototype") 实现“每次请求都生成新对象”的核心机制,其实非常简单直接:
它完全不缓存 bean 实例。
与单例(singleton)不同,spring 容器内部没有一个 map 来存储 prototype bean 的实例。每次你调用 getbean() 或注入该 bean 时,spring 都会重新执行完整的创建流程。
1. 核心流程对比
为了让你看清区别,我们对比一下 singleton 和 prototype 的处理逻辑:
🔵 单例 (singleton) - “查缓存,没有才造”
// 伪代码:spring 内部逻辑
public object getbean(string beanname) {
// 1. 先查一级缓存 (singletonobjects)
object bean = singletonobjects.get(beanname);
// 2. 如果有,直接返回 (秒回,不创建)
if (bean != null) {
return bean;
}
// 3. 如果没有,才创建,并放入缓存
bean = createbean(beanname);
singletonobjects.put(beanname, bean); // <--- 关键:存起来
return bean;
}结果:第一次创建,后续无数次都是返回同一个对象。
🟢 多例 (prototype) - “不管有没有,每次都造”
// 伪代码:spring 内部逻辑 (abstractbeanfactory.dogetbean)
public object getbean(string beanname) {
// 1. 获取 bean 定义
rootbeandefinition mbd = getmergedlocalbeandefinition(beanname);
// 2. 判断作用域
if (mbd.isprototype()) {
// 【关键点】:直接进入创建流程,完全不查缓存!
// 也不会在创建后存入任何缓存
return createbean(beanname, mbd, args);
}
// ... 单例的其他逻辑 ...
}结果:每次调用 createbean,都是一次全新的生命周期(实例化 -> 属性填充 -> 初始化)。
2. 详细执行步骤
当你请求一个 prototype bean 时,spring 内部会发生以下过程:
- 解析定义:读取 xml 或注解中的
@scope("prototype")配置。 - 跳过缓存检查:代码逻辑直接判定为多例,跳过
getsingleton()这种缓存查找操作。 - 调用
createbean():- 实例化 (instantiation):调用构造函数(或工厂方法)
new yourclass()。 - 属性填充 (populate):依赖注入 (
@autowired,@value)。 - 初始化 (initialization):执行
@postconstruct,initializingbean.afterpropertiesset(), 自定义init-method。
- 实例化 (instantiation):调用构造函数(或工厂方法)
- 返回对象:将创建好的全新对象返回给调用者。
- 放手不管:spring 容器不再持有该对象的引用。
- 这意味着:spring 不会管理 prototype bean 的完整生命周期。
- 特别是:spring 不会自动调用
@predestroy或destroy-method。因为容器不知道这个对象去哪了,也不知道什么时候该销毁它(由调用者负责垃圾回收)。
3. 源码级证据
在 spring 的核心类 abstractbeanfactory 的 dogetbean 方法中,有非常明确的逻辑分支:
// org.springframework.beans.factory.support.abstractbeanfactory.java
protected <t> t dogetbean(...) {
// ... 前置处理 ...
// 判断作用域
string scopename = mbd.getscope();
if (scopename != null) {
if (scopename.equals(configurablebeanfactory.scope_prototype)) {
// 【核心代码】
// 如果是 prototype,直接调用 createbean,没有任何缓存逻辑
object bean = createbean(beanname, mbd, args);
return (t) bean;
}
// 如果是 request/session 等其它作用域,会使用 scope 接口处理
else if (scopename.equals(configurablebeanfactory.scope_request)) {
// ...
}
}
// 如果是 singleton (默认),走下面的缓存逻辑
// ... sharedinstance = getsingleton(...) ...
}可以看到,对于 scope_prototype,代码路径是最短、最直接的,直接调用了创建方法。
4. 常见误区与坑
❌ 误区 1:以为注入到单例 bean 中也能每次变新
这是最常见的错误!
@component // 默认是 singleton
public class userservice {
@autowired
private actionservice actionservice; // 假设 actionservice 是 prototype
}- 现象:
userservice是单例,它在 spring 启动时只会被创建一次。 - 结果:在创建
userservice的那一瞬间,spring 注入了一个actionservice实例。此后,无论你怎么用userservice,里面的actionservice永远是同一个对象,不会变! - 原因:依赖注入发生在 bean 创建时。单例 bean 只创建一次,所以依赖也只注入一次。
✅ 解决方案:如何在一个单例中获取多例?
如果你需要在单例中每次都用新的 prototype 对象,有三种方法:
方法注入 (lookup method injection) - 推荐,纯注解
使用 @lookup 注解,spring 会在运行时动态重写这个方法,每次调用都去容器 getbean
@component
public class userservice {
@lookup
protected actionservice createactionservice() {
// 方法体可以是空的,spring 会覆盖它
return null;
}
public void dowork() {
actionservice action = createactionservice(); // 每次都是新的
}
}注入 objectfactory 或 provider - 推荐,标准写法
注入一个工厂对象,需要时手动 getobject()
@component
public class userservice {
@autowired
private objectprovider<actionservice> actionprovider;
public void dowork() {
actionservice action = actionprovider.getobject(); // 每次都是新的
}
}直接从 applicationcontext 获取 - 不推荐,耦合度
@autowired
private applicationcontext context;
public void dowork() {
actionservice action = context.getbean(actionservice.class);
}4.是要@lazy注解标记多例对象
@lazy启动时候创建代理类,在调用时候执行获取对象,生成代理对象,走getbean,由于标注多例,则会重写createbean创建新的对象
| 特性 | @lazy 注入 | objectprovider 注入 | @lookup 注解 |
|---|---|---|---|
| 能否获取新实例 | ✅ 能 (spring 5+) | ✅ 能 | ✅ 能 |
| 代码可读性 | ⭐⭐⭐ (意图是“延迟”,多例是隐含的) | ⭐⭐⭐⭐⭐ (意图明确:“我要工厂”) | ⭐⭐⭐⭐ (意图明确,但需抽象方法) |
| 灵活性 | ⭐⭐ (只能直接调用方法) | ⭐⭐⭐⭐⭐ (可判断 ifavailable, 流式操作 stream()) | ⭐⭐ (只能获取) |
| 底层原理 | aop 代理 (jdk 或 cglib) | 依赖注入工厂对象 | cglib 方法重写 |
| 推荐指数 | 推荐 (简单场景够用) | 强烈推荐 (最佳实践) | 推荐 (无参构造场景) |
5. 总结
spring 多例对象之所以能每次生成新的,是因为:
- 策略不同:它在
dogetbean阶段就跳过了所有缓存检查。 - 动作直接:直接调用
createbean()执行完整的实例化和初始化流程。 - 无状态管理:创建完成后,容器立即断开引用,不保存、不跟踪、不销毁。
这就好比自动售货机(单例)和现做冰淇淋摊(多例):
- 自动售货机:货都在柜子里(缓存),有人买直接拿,卖完了才补货。
- 冰淇淋摊:每次有人买,老板都重新挖球、装筒(createbean),做完给你,手里不留货(不缓存)。
到此这篇关于spring单例类加载多例属性问题的文章就介绍到这了,更多相关spring单例类加载多例属性内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论