Spring 中 Bean 的作用域有哪些?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你能背出几种作用域,更是想看你能否准确描述每种作用域的生命周期和适用场景,能不能说出 singletonprototype 的核心区别。

  2. 实践经验:考察你是否在真实项目中用过非 singleton 的作用域,比如 Web 环境下的 requestsession 作用域,有没有踩过坑。

  3. 原理理解:是否理解 singleton 下注入 prototype Bean 的 "陷阱",以及解决方案,这才是拉开差距的地方。

核心答案

Spring 框架支持 6 种 Bean 作用域,其中前 2 种是所有环境都可用的,后 4 种只在 Web 环境下生效——

作用域 说明 适用环境
singleton 默认值。整个 IoC 容器中只有一个 Bean 实例 所有环境
prototype 每次获取 Bean 时都创建一个新实例 所有环境
request 每个 HTTP 请求创建一个实例,请求结束即销毁 仅 Web
session 每个 HTTP Session 创建一个实例,Session 结束即销毁 仅 Web
application 每个 ServletContext 对应一个实例 仅 Web
websocket 每个 WebSocket 会话对应一个实例 仅 Web

深度解析

一、singleton — 默认但别无脑用

singleton 是 Spring 的默认作用域,意思是 Spring 容器启动时(严格来说是首次被请求时)创建一个实例,之后所有地方拿到的都是同一个对象。

// 默认就是 singleton,写不写都一样
@Component
public class UserService {
    // ...
}

// 显式指定
@Component
@Scope("singleton")
public class UserService {
    // ...
}

大多数业务场景用 singleton 没问题,因为 Spring Bean 通常是无状态的(不保存会话数据),靠方法参数和 ThreadLocal 来处理并发。但如果你在 singleton Bean 里存了可变状态,那就是给自己埋雷——多线程并发访问会出问题。

二、prototype — 用得少但得懂

prototype 作用域下,每次从容器中获取 Bean(通过 getBean() 或依赖注入),都会拿到一个全新的实例。Spring 只负责创建,不负责销毁,生命周期管理交给了使用者。

@Component
@Scope("prototype")
public class PaymentTask {
    private String orderId;

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }
}

适合那些有状态的、需要独立副本的对象,比如每次请求都要携带不同参数的任务对象。

三、Web 环境的四种作用域

这四个只在 Web 环境下才有意义,底层依赖 ServletRequestHttpSessionServletContext 等 Web 对象。

上图展示了四种 Web 作用域的生命周期,核心区别在于 "绑定的生命周期对象" 不同:

  • request:绑定一次 HTTP 请求。请求进来时创建,响应返回时销毁。适合存放请求级别的数据,比如当前请求的用户信息。
  • session:绑定一次 HTTP 会话。用户首次访问创建,Session 过期或手动销毁时结束。适合存放会话级别的数据,比如购物车。
  • application:绑定 ServletContext,和 Web 应用同生命周期。效果类似 singleton,但 singleton 是每个 Spring 容器一个,application 是每个 ServletContext 一个(理论上一个 Web 应用可能有多个 Spring 容器)。
  • websocket:Spring 4.2 新增,绑定 WebSocket 会话。适合需要在 WebSocket 连接期间保持状态的场景。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String userId;
    // 每个请求独立的上下文
}

注意那个 proxyMode,下面细说为什么需要它。

四、singleton 里注入 prototype 的 "陷阱"

这个是面试官最爱追问的点。假设你有一个 singleton 的 Service,里面注入了一个 prototype 的 Bean——

@Component // 默认 singleton
public class OrderService {

    @Autowired
    private PaymentTask paymentTask; // prototype

    public void process(String orderId) {
        paymentTask.setOrderId(orderId);
        paymentTask.execute();
    }
}

你以为每次调用 process() 都会用新的 PaymentTask错了。因为 OrderService 是 singleton,Spring 只会在创建它的时候注入一次 PaymentTask,之后用的永远是同一个实例。prototype 根本没起作用。

三种解决方案:

方案一:使用 @Lookup 方法注入(推荐)

@Component
public abstract class OrderService {

    @Lookup  // Spring 会通过 CGLIB 生成子类重写这个方法
    public abstract PaymentTask getPaymentTask();

    public void process(String orderId) {
        PaymentTask task = getPaymentTask(); // 每次拿到新实例
        task.setOrderId(orderId);
        task.execute();
    }
}

方案二:使用 ObjectFactory / Provider

@Component
public class OrderService {

    @Autowired
    private ObjectFactory<PaymentTask> paymentTaskFactory;

    public void process(String orderId) {
        PaymentTask task = paymentTaskFactory.getObject(); // 每次拿到新实例
        task.setOrderId(orderId);
        task.execute();
    }
}

方案三:使用 Scoped Proxy(代理模式)

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

加了 proxyMode 后,Spring 注入的其实是一个代理对象,每次调用方法时代理会去容器拿真正的 prototype 实例。Web 作用域(requestsession)也需要用这种方式,因为 singleton Bean 创建时可能还没有 Request/Session 上下文。

五、如何指定作用域

// 方式一:注解
@Component
@Scope("prototype")
public class MyBean {}

// 方式二:Java Config
@Configuration
public class AppConfig {
    @Bean
    @Scope("prototype")
    public MyBean myBean() {
        return new MyBean();
    }
}

// 方式三:XML(现在很少用了)
<bean id="myBean" class="com.example.MyBean" scope="prototype" />

面试高频追问

  1. 追问一:singleton Bean 是线程安全的吗?

    不是。singleton 只保证容器中只有一个实例,但如果 Bean 内部有可变状态,多线程并发修改就会出问题。通常的做法是:Bean 设计成无状态的,或者用 ThreadLocal,或者通过 prototype 作用域来避免共享。

  2. 追问二:Spring 的 singleton 和 GOF 设计模式中的单例模式有什么区别?

    GOF 单例是类加载级别的,一个 ClassLoader 只有一个实例,通过私有构造函数实现。Spring 的 singleton 是容器级别的,一个 Spring IoC 容器中只有一个实例,但你可以自己 new 一个同类型的对象,不冲突。Spring 单例的实现靠的是容器管理,而不是私有构造函数。

  3. 追问三:request 作用域的 Bean 在非 Web 环境下能用吗?

    不能。会抛出 IllegalStateException,因为这些作用域依赖 Web 环境的 RequestContextHolder。如果你需要在非 Web 环境下实现类似效果,可以考虑自定义作用域(实现 Scope 接口)。

常见面试变体

  • "Spring Bean 默认是什么作用域?能改成其他的吗?"
  • "singleton 里注入 prototype 会怎样?怎么解决?"
  • "requestsession 作用域有什么区别?"

记忆口诀

六种作用域:单(singleton)、多(prototype)、请(request)、会(session)、应(application)、ws(websocket)。前两个通用,后四个限定 Web。

singleton 注 prototype 的坑:单例只注入一次,原型变 "假原型"。解决靠 @LookupObjectFactory、代理三把斧。

总结

Spring 的 6 种 Bean 作用域,核心掌握 singletonprototype 的区别就够了。生产中 99% 的场景用的都是 singleton,但面试官真正想考察的是你知不知道 "singleton 注入 prototype 的陷阱" 以及对应的解决方案。把这个点讲清楚,这道题基本满分。