SpringPrototype作用域

Spring @Scope("prototype") 作用域:从「每次新建」到那些坑与冷知识

适用版本:Spring Framework 5.x / 6.x(Java 8+)。本文结论均以 Spring 官方文档(Bean Scopes / Method Injection / @Bean Javadoc)为准。

配套前置:本系列已讲「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 销毁不归容器)
    }
}

看什么:

  1. SharedService 两次 cmdId() 相同 → 证明单例注入的 prototype 被共享;
  2. FreshService 两次 freshId() 不同 → ObjectFactory 每次拿新的;
  3. ctx.close() 后没有任何 destroy 日志 → 印证 prototype 销毁回调不触发。

六、总结(一口答)

  1. prototype = 每次 getBean/注入解析都新建,不按线程划分;
  2. 注入单例只创建一次,单例终身持有 → 多线程共享,不是隔离;
  3. 想运行时反复拿新实例:用 ObjectFactory / Provider / ApplicationContext.getBean / @Lookup / 作用域代理;
  4. prototype 只给实例隔离 ,不给自动线程安全 ;真线程隔离用 ThreadLocal 或 @RequestScope;
  5. 销毁回调(@PreDestroy / DisposableBean / destroy-method)不归容器管,初始化回调仍执行;
  6. 实际适用面窄:多数「要新实例」直接 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),你本地一键跑通三类对比。

相关推荐
大风起兮云飞扬丶1 小时前
Java——枚举
java·开发语言
ReStock活库2 小时前
Zephyr异步SPI调用返回后,为什么不能立即复用发送缓冲区?
java·前端·网络
微微%2 小时前
从零搭建一个java开发环境
java·idea2023·jdk17
SimonKing2 小时前
又找到一个纯Java的OCR项目,识别的格式还挺多
java·后端·程序员
lengxuenong2 小时前
区间覆盖问题 · 简明学习手册1
java·学习·算法
?abc!2 小时前
(贪心)621. 任务调度器
java·前端·c#
jason成都3 小时前
rtklib_java PPP-RTK 收官测试:修复 BDS-2 宽巷失效与异常卫星死循环 Bug
java·gnss·rtklib·rtklib_java
Flynn Liang3 小时前
Spring AI ChatClient 详解:system/user/assistant 角色、call 与 stream 到底怎么选
java·人工智能·spring·springai·流式输出·chatclient·java接入大模型
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 喜欢与取消喜欢
java·开发语言·交友