Skip to content

Spring Bean 作用域:singleton/prototype 与代理注入

问题

两个面试必问题:Bean 的六种作用域各有什么特性?为什么单例 Bean 里注入一个 prototype Bean 每次拿到的却是同一个对象?

六种作用域怎么选的

Spring 从 2.0 开始只有 singleton 和 prototype,3.0 引入 web 作用域(request/session/application),5.0 加入 websocket。六种一到,面试考到"哪个作用域常用"时,别只答前两个。

作用域实例数生命周期适用范围
singleton整个容器一个容器启动→容器关闭绝大多数 Bean(默认),无状态组件
prototype每次 getBean 创建一个容器创建→交给你后不管有状态对象、模板实例
request每个 HTTP 请求一个请求开始→请求结束Web 上下文中的请求级状态
session每个 HTTP Session 一个会话开始→会话结束用户会话级状态
application每个 ServletContext 一个应用启动→应用关闭和 singleton 同一级别但绑定 ServletContext
websocket每个 WebSocket 会话一个连接建立→连接关闭WebSocket 会话级状态

singleton 占 95% 以上,因为大部分 Bean 无状态,共享一个实例省内存也省创建开销。prototype 只在明确需要每次新建的场景用:比如一个模板类持有每次调用不同的参数,或者一个消息处理器每次处理前需要重置状态。web 作用域多数场景被 scope proxy 替代,后面会讲到。

千万别踩的坑:单例注入原型

开发中经常会遇到这类代码:

java
@Component
@Scope("prototype")
public class PriceCalculator {
    private final UUID instanceId = UUID.randomUUID();
    public void calculate() {
        System.out.println("Calculator instance: " + instanceId);
    }
}

@Component
public class OrderService {
    @Autowired
    private PriceCalculator calculator;
    public void processOrder() {
        calculator.calculate();
    }
}

猜猜每次调用 processOrder() 输出的 instanceId 是什么?永远一样

原因很简单:OrderService 是单例,在容器初始化阶段只执行一次属性注入。Spring 调用 getBean("priceCalculator") 创建一个 prototype 实例注入进去,然后这个实例就被固定了。此后每次 processOrder() 拿到的都是同一个 PriceCalculator 对象,prototype 根本没生效。

两种解法:ObjectFactory 与代理

解法一:ObjectFactory / Provider

把延迟获取交给容器,每次调用时主动拿一个新实例:

java
@Component
public class OrderService {
    @Autowired
    private ObjectFactory<PriceCalculator> calculatorFactory;

    public void processOrder() {
        PriceCalculator calculator = calculatorFactory.getObject();
        calculator.calculate();
    }
}

每次调用 getObject() 都会走 BeanFactory.getBean(),触发 prototype 的创建流程。javax.inject.Provider 效果一样,是 JSR-330 的标准化接口。显式、可控,没有代理开销。

解法二:作用域代理(Scoped Proxy)

@ScopeproxyMode 参数让 Spring 在注入时不是放真实 Bean,而是放一个代理对象:

java
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PriceCalculator {
    // ...
}

OrderService 里照常 @Autowired,注入的是一个 CGLIB 动态生成的子类代理。每次调用 calculator.calculate(),代理都会走到 CglibAopProxy 拦截器,从容器里重新获取一个实例来执行方法。这样调用方不需要改代码,业务逻辑和获取逻辑解耦了。

java
// 代理最终生成的类伪代码
public class PriceCalculator$$EnhancerBySpringCGLIB extends PriceCalculator {
    @Override
    public void calculate() {
        PriceCalculator real = getBeanFromApplicationContext(); // 每次新实例
        real.calculate();
    }
}

TARGET_CLASS 用 CGLIB 生成子类代理,INTERFACES 用 JDK 动态代理。原型作用域用 TARGET_CLASS 最保险,因为目标类不一定实现了接口。

选型:ObjectFactory 零开销、显式,适合调用方知道自己要新实例的场景;proxyMode 对调用方透明,但每次方法调用都有拦截开销。个人倾向于 ObjectFactory,显式依赖比隐式代理容易排查。

prototype 的销毁回调不执行

题目:@Scope("prototype") 上加了 @PreDestroy,容器关闭时执行了吗?

不执行。容器创建后就不持有 prototype 实例的引用,无法在关闭时遍历销毁。销毁逻辑只对 singletonObjects 里的单例生效。原型 Bean 的资源释放自己管,比如在调用方显式调用 close(),或通过 @Bean(destroyMethod = "close")

两个 web 作用域的真相

request/session/application 作用域必须在 WebApplicationContext 中才能使用。在非 web 环境(比如单元测试)下直接声明 @Scope("request") 启动会报 IllegalStateException,因为对应的 Scope 实现没有注册。Spring Boot 测试通过 @WebAppConfiguration 解决。

实际开发中,request 作用域最常见的场景是把当前请求的用户信息、TraceId 通过 Filter 放到 RequestContextHolder 里,而不是真把 Bean 设成 request 作用域。RequestContextHolder 背后是 ThreadLocal,比 Scope 代理更轻量,也不依赖 Spring 容器的作用域解析。

几种作用域注入方案对比

把上面涉及的技术放到一起看:

方案每次新实例对调用方透明开销适合场景
原型注入(@Autowired prototype)❌ 不生效透明错误用法,不要写
ObjectFactory需显式调用每次 getBean 创建调用方明确知道需要新实例
Scope Proxy透明每次方法调用都走拦截遗留代码/团队一致性要求高
ThreadLocal线程级需手动 get/set无拦截,线程级存储请求级上下文、TraceId 传递

总结

singleton 是默认,prototype 是例外,但例外往往藏着面试题。单例注入原型不生效,解法是 ObjectFactory 或 proxyMode,选 ObjectFactory 更可控。prototype 销毁回调不执行,资源自己管。web 作用域多数场景用 RequestContextHolder 替代。代理方便,但隐式拦截路径在排查性能问题时容易被忽略——优先显式方案。

参考

  • Spring 官方文档:Bean Scopes — The @Scope annotation
  • AbstractBeanFactory#getBeanScope 接口实现源码
  • SimpleThreadScopeAbstractRequestAttributesScope 实现对比
  • ScopedProxyUtils#createScopedProxy 代理生成逻辑

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1