Spring 中 Bean 的作用域有哪些?

#Java#面试八股

Spring 中 Bean 的作用域有哪些?

结论先行:Spring 内置六种作用域:singleton(默认,容器内唯一)、prototype(每次获取新建)、以及仅在 Web 环境生效的 request、session、application、websocket。作用域决定"实例的数量与生命周期归属",不决定线程安全;单例注入短生命周期 Bean 时必须配 proxyMode 生成作用域代理,否则短生命周期对象会被"钉"在单例里。

一、六种作用域

作用域实例数量生命周期生效条件典型用途
singleton每容器一个随容器创建与销毁默认无状态 Service、DAO、配置
prototype每次 getBean/注入一个容器不管理销毁默认有状态、短命对象
request每 HTTP 请求一个请求结束销毁Web 环境请求级上下文、参数聚合
session每会话一个会话过期销毁Web 环境登录用户信息(不推荐存大对象)
application每 ServletContext 一个应用生命周期Web 环境全局缓存、共享配置
websocket每 WebSocket 会话一个连接关闭销毁WebSocket 环境长连接会话状态

二、声明方式

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestHolder {
    private String traceId;
    // getter / setter
}
<!-- XML 写法,proxy-mode 等价于注解的 proxyMode -->
<bean id="holder" class="com.demo.RequestHolder" scope="request" proxy-mode="targetClass"/>

三、关键坑:单例注入短生命周期

单例 OrderService ──持有──▶ RequestHolder(request 作用域)
   ▲                              ▲
   │ 容器启动时注入一次            │ 若没有代理,这个引用永远是"第一个请求"的对象
   └──────────────────────────────┘

容器启动时单例初始化,此时并没有 HTTP 请求,注入只能得到"某个代理"或直接报错。proxyMode 的解法是注入一个 CGLIB/JDK 代理,代理在每次方法调用时从 RequestAttributes 中取当前请求对应的真实实例。

@Service
public class OrderService {
    // 注入的是代理对象,调用时才解析到当前请求的 RequestHolder
    private final RequestHolder holder;

    public OrderService(RequestHolder holder) {
        this.holder = holder;
    }

    public void handle() {
        System.out.println(holder.getTraceId());   // 代理转发到当前请求实例
    }
}

四、两个易错点

  1. prototype 的销毁不由容器负责:容器只负责实例化与初始化,@PreDestroy 不会回调,需要调用方自己释放资源(或实现 DisposableBean 后手工调用)。
  2. singleton 与 GoF 单例不是一回事:Spring 的 singleton 是"每个容器每个 Bean 名一个实例",同一个类可以在不同容器、不同 Bean 名(甚至同容器不同 id)下存在多个实例;GoF 单例是 JVM 级、类加载器级唯一。
// 第二种解法:ObjectProvider 延迟查找,不依赖作用域代理
@Service
public class ReportService {
    private final ObjectProvider<RequestHolder> holderProvider;

    public ReportService(ObjectProvider<RequestHolder> holderProvider) {
        this.holderProvider = holderProvider;
    }

    public String traceId() {
        // 每次调用时才向容器要实例,request 作用域下即当前请求对应的对象
        return holderProvider.getIfAvailable(RequestHolder::new).getTraceId();
    }
}

ObjectProvider 适合"用一次取一次"的场景;proxyMode 适合"注入后到处当普通字段用"的场景,二者不要叠加使用。

五、常见追问

  • 问:request 作用域在非 Web 环境会怎样? 抛 IllegalStateException: No thread-bound request found,因为拿不到 RequestAttributes。
  • 问:作用域代理的底层是什么? ScopedProxyFactoryBean + SimpleBeanTargetSource,本质仍是 AOP 代理:每次调用从 RequestContextHolder 或会话中查找目标对象。
  • 问:@Scope 与 proxyMode 能用在 @Bean 方法上吗? 可以,例如 @Bean @Scope(value="request", proxyMode=TARGET_CLASS)。但 @Bean 方法本身在配置类中被调用时仍返回新对象,需注意代理语义。
  • 问:线程作用域存在吗? Spring 提供 SimpleThreadScope,但不会默认注册,需要 CustomScopeConfigurer 手动加,且线程池场景几乎不可用。

小结

作用域回答"这个 Bean 有几个、活多久、谁来销毁"。面试时先报六种,再重点讲清两件事:prototype 不回调销毁、单例注入 request/session 必须 proxyMode。这两点几乎覆盖了作用域相关的全部线上事故。

笔记加载中…