Spring @Scope("prototype") 作用域:从「每次新建」到那些坑与冷知识
适用版本:Spring Framework 5.x / 6.x(Java 8+)。本文结论均以 Spring 官方文档(Bean Scopes / Method Injection /
@BeanJavadoc)为准。配套前置:本系列已讲「bean 生命周期 init/destroy 三步」,本文会直接复用其中结论。
一、背景(为什么要有 prototype)
Spring 默认所有 bean 都是 singleton (容器内单例,复用、省内存)。但有些对象有状态、且不能在线程间共享------比如一次业务请求内临时用的「命令对象 / 策略对象 / 批量上下文」。如果也用单例,多个线程会互相踩状态。
prototype(原型)作用域就是为这种场景设计的:每次从容器获取,都给你一个新实例 。它相当于「容器替你 new」,但对象一旦交给客户端,容器就不再管它的死活。
java
@Component
@Scope("prototype") // 或 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Command { /* 有状态,不可共享 */ }
二、初步了解(prototype 是什么)
| 维度 | 说明 |
|---|---|
| 创建时机 | 每次 getBean() / 每次依赖注入点解析,都 new 一个 |
| 数量 | getBean 调 N 次 → N 个独立实例 |
| 容器职责 | 实例化、填充属性、走初始化回调,然后丢弃(不再追踪) |
| 默认 scope | 否,默认是 singleton,需显式 @Scope("prototype") |
| 典型用途 | 有状态、不可共享、用完即弃的对象(命令/策略/临时上下文) |
⚠️ 第一个常见误解(必须纠正) :prototype 不是「每线程一份」。它的语义是「每次容器被请求就新建」,与线程无关。把它当成线程隔离是面试和实战里最高频的误用。
三、深度了解(两个核心坑 + 四种正解)
3.1 坑一:prototype 注入单例,单例终身持有同一份
依赖在单例实例化时解析一次 。所以把 prototype 注入单例,单例拿到的那个 prototype 实例固定不变 ------之后所有线程调这个单例方法,共享的是同一个 prototype 实例,不仅没隔离,反而多线程共享。
单例 Service 创建时 ──注入──> prototype P (只 new 这一次)
线程A 调 service.xxx() ─┐
线程B 调 service.xxx() ─┼──> 都用同一个 P 实例(共享,非隔离!)
线程C 调 service.xxx() ─┘
这正是你原话里的「若被单例注入后仍是单例持有(只创建一次)」------完全正确,它直接推翻了「每线程独享」的说法。
3.2 坑二:prototype ≠ 自动线程安全
即使你真的每次都拿到新实例,prototype 也只给「实例隔离」,不给自动线程安全:
- prototype 里有个
SimpleDateFormat字段 → 每实例各一份 → 该字段安全; - 但若 prototype 写静态变量 、或持有共享连接 / 共享 Map → 照样不安全;
- 真要「每线程一份」,正确工具是
ThreadLocal或@RequestScope(Web 下每请求一份,通常即每线程一份),不是 prototype。
3.3 四种正解:想「运行时反复拿新实例」怎么办
① ObjectFactory / Provider(最轻,推荐)
java
@Component
public class SingletonService {
@Autowired
private ObjectFactory<Command> commandFactory;
public void handle() {
Command c = commandFactory.getObject(); // 每次调用 → 新实例
c.execute();
}
}
javax.inject.Provider语义相同(provider.get());Spring 的ObjectProvider还支持流式/可选获取。
② ApplicationContext.getBean(简单但耦合容器)
java
@Autowired private ApplicationContext ctx;
public void handle() {
Command c = ctx.getBean(Command.class); // 每次新实例
}
③ @Lookup 方法注入(Spring 为 prototype 专设的机制)
java
@Component
public abstract class CommandManager {
public Object process(Object state) {
Command c = createCommand(); // 每次新实例
c.setState(state);
return c.execute();
}
@Lookup // 不传名则按返回类型解析
protected abstract Command createCommand();
}
容器用 CGLIB 重写
createCommand(),内部走getBean。注意:目标 bean 必须部署为 prototype,否则每次返回同一个单例(官方文档原文警告)。
④ 作用域代理 proxyMode(注入接口/类,调用时解析)
java
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class Command { /* ... */ }
@Component
public class SingletonService {
@Autowired
private Command command; // 注入的是代理(代理本身是单例)
}
⚠️ 代理模式下,每次调用
command的方法才会解析出一个新目标实例------这是「per-call(每次调用)」而非「per-thread」,别再混淆。
四、源码了解(scope 在哪儿被解析)
- scope 判定 :
AbstractBeanFactory.doGetBean()里按BeanDefinition.getScope()分支;prototype 走createBean()每次新建并返回,且不会 把实例放进单例缓存singletonObjects。 - 依赖解析一次 :单例的
AbstractAutowireCapableBeanFactory.createBean()在填充属性(populateBean)时一次性解析 prototype 依赖,之后单例对象持有该引用,不再二次请求。 @Configuration增强 :@Configuration(proxyBeanMethods=true)(默认)会被 CGLIB 子类化,类内@Bean方法互相调用会被拦截并路由到容器 (尊重 scope);而 lite 模式 (@Component含@Bean,或@Configuration(proxyBeanMethods=false))下,类内@Bean方法直接调用是普通 Java 调用,每次 new,且 scope 注解不被尊重------详见第七节。- 销毁不追踪 :prototype 创建后容器不保留引用,
DisposableBeanAdapter的销毁逻辑对 prototype 不会触发(见 3.1→第七节联动)。
五、demo 实现(可跑:验证「单例持有 vs 每次新建」)
java
@Scope("prototype")
@Component
public class Command {
private final String id = "CMD@" + Integer.toHexString(System.identityHashCode(this));
@PostConstruct public void init() { System.out.println("init " + id); }
@PreDestroy public void destroy(){ System.out.println("destroy " + id); } // 不会打印!
public String id() { return id; }
}
@Component
public class SharedService { // 单例
@Autowired private Command cmd; // 注入一次,终身持有
public String cmdId() { return cmd.id(); }
}
@Component
public class FreshService { // 单例
@Autowired private ObjectFactory<Command> factory;
public String freshId() { return factory.getObject().id(); }
}
public class Demo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext("com.demo");
SharedService shared = ctx.getBean(SharedService.class);
System.out.println(shared.cmdId()); // CMD@a1
System.out.println(shared.cmdId()); // CMD@a1 ← 同一个!共享,非隔离
FreshService fresh = ctx.getBean(FreshService.class);
System.out.println(fresh.freshId()); // CMD@b2
System.out.println(fresh.freshId()); // CMD@b3 ← 每次新实例
ctx.close(); // 观察:没有 "destroy" 输出(prototype 销毁不归容器)
}
}
看什么:
SharedService两次cmdId()相同 → 证明单例注入的 prototype 被共享;FreshService两次freshId()不同 →ObjectFactory每次拿新的;ctx.close()后没有任何destroy日志 → 印证 prototype 销毁回调不触发。
六、总结(一口答)
- prototype = 每次 getBean/注入解析都新建,不按线程划分;
- 注入单例只创建一次,单例终身持有 → 多线程共享,不是隔离;
- 想运行时反复拿新实例:用
ObjectFactory/Provider/ApplicationContext.getBean/@Lookup/ 作用域代理; - prototype 只给实例隔离 ,不给自动线程安全 ;真线程隔离用
ThreadLocal或@RequestScope; - 销毁回调(@PreDestroy / DisposableBean / destroy-method)不归容器管,初始化回调仍执行;
- 实际适用面窄:多数「要新实例」直接
new更清晰,per-request 用@RequestScope,滥用 prototype 反而增加状态管理与测试成本。
七、其他常见知识点(必记清单)
7.1 销毁不归容器管,但初始化照常执行(联动 bean 生命周期)
- prototype 的 init 三步(@PostConstruct / InitializingBean / init-method)每次新建都执行;
- 但 destroy 三步(@PreDestroy / DisposableBean / destroy-method)永远不会被容器调用------因为容器创建后就不持有引用。
- 释放昂贵资源(连接、文件)要客户端自己处理,或借助实现了
DestructionAwareBeanPostProcessor的自定义后处理器持有引用去清理。 - 记忆锚点:「容器对 prototype 的角色 ≈ Java 的
new运算符,之后的生命周期全由客户端负责」。
7.2 @Configuration 的 @Bean 内部调用:full vs lite 模式(高频坑)
| 写法 | 模式 | 类内 a() 调 b() 的行为 |
scope 尊重 |
|---|---|---|---|
@Configuration(默认 proxyBeanMethods=true) |
full | CGLIB 拦截,路由到容器(singleton 返回同一,prototype 返回新) | ✅ 尊重 |
@Configuration(proxyBeanMethods=false) |
lite | 普通 Java 调用,每次 new | ❌ 不尊重 |
@Component 里写 @Bean |
lite | 同上,每次 new | ❌ 不尊重 |
实战坑:在 lite 模式下,哪怕
b()标了@Scope("prototype"),a()里return new X(b())也会每次 new 一个 b ,且不会被容器托管/代理。要依赖另一个 bean,请写成方法参数@Bean X a(B b),让容器注入,而非直接调用。
7.3 作用域代理 proxyMode 的「per-call」语义
@Scope(value="prototype", proxyMode=TARGET_CLASS):注入的是代理 (代理本身通常随宿主是单例),但每次调用代理的方法都解析一个新目标实例。- 副作用:目标实例的构造时机在方法调用时,不是注入时;且若 prototype 有构造函数里要注入的依赖,每次调用都会触发一次完整创建。
7.4 与 request / session 作用域对比
| scope | 生命周期边界 | 是否需要 Web 容器 |
|---|---|---|
| singleton | 容器级,全程一份 | 否 |
| prototype | 每次 getBean 一份,容器不追踪 | 否 |
| request | 每个 HTTP 请求一份(通常即每线程一份) | 是(WebApplicationContext) |
| session | 每个 HTTP 会话一份 | 是 |
需要「每请求隔离」用
@RequestScope,别硬套 prototype。
7.5 循环依赖:prototype 参与会直接抛异常
- Spring 用
prototypesCurrentlyInCreation记录「正在创建的 prototype」;若 A→B→A 都是 prototype,第二次请求 A 时发现它正在创建 → 抛BeanCurrentlyInCreationException。 - 对比 singleton:singleton 靠「早期引用(early reference)」能容错循环;prototype 没有这套机制,不支持循环依赖。
7.6 AOP 代理下每个实例独立
- 若 prototype bean 被 AOP 增强,容器会为每个新实例包一层代理;代理随实例走,不存在「一个代理服务多个目标」。这与作用域代理(7.3)是两回事。
7.7 @Lookup 与 ObjectFactory 怎么选
@Lookup:语义最贴合「方法需要新实例」,但要求宿主是 Spring bean 且通常用抽象方法 + CGLIB 重写;ObjectFactory/Provider:更轻、不要求改写方法,推荐日常使用;ObjectProvider还能配合getIfAvailable()/orderedStream()做可选/集合注入。- 两者都要求目标 bean 是 prototype,否则都返回同一单例。
7.8 性能代价
- 无缓存、每次新建 + GC 回收,高频创建大对象会抬升 GC 压力;
- 若对象无状态,用 singleton 反而更优(典型如 DAO、Service 本就无会话状态,官方文档明确不建议配成 prototype)。
八、事实核验与自检报告
| # | 断言 | 判定 | 官方出处 |
|---|---|---|---|
| 1 | prototype 每次 getBean/注入解析都新建实例 | 成立 | Spring 文档 Bean Scopes |
| 2 | prototype 注入单例只创建一次、单例终身持有 | 成立 | Spring 文档「Singleton beans with prototype-bean dependencies」 |
| 3 | prototype 销毁回调不归容器管(@PreDestroy 等不触发) | 成立 | Spring 文档 Bean Scopes(明确:destruction lifecycle callbacks not called) |
| 4 | prototype 初始化回调仍执行 | 成立 | 同上(init callbacks called on all objects regardless of scope) |
| 5 | @Lookup 目标须为 prototype,否则返回同一单例 |
成立 | Spring 文档 Method Injection |
| 6 | lite 模式下 @Bean 内部调用是普通 Java 调用、不尊重 scope |
成立 | @Bean Javadoc / Spring 文档「@Bean and @Configuration」(lite mode) |
| 7 | 作用域代理为 per-call 解析新目标 | 成立 | Spring 文档 Scoped Beans as Dependencies |
| 8 | prototype 参与循环依赖抛 BeanCurrentlyInCreationException |
成立(源码行为,官方文档未专述,由 prototypesCurrentlyInCreation 机制推导) |
AbstractBeanFactory 源码 |
总判定:8 条断言全部可溯源至 Spring 官方文档或框架源码,无错误断言;第 8 条为源码行为推导,已在表中注明。
待你拍板(未执行) :本文为方案/文档,未建立可运行工程文件;若需要,我可把第五节 demo 落成完整 Spring Boot 工程(含 pom.xml、@SpringBootApplication、命令行 runner),你本地一键跑通三类对比。