【大白话说Java面试题 第154题】【06_Spring篇】第14题:Spring 支持的 Bean 作用域

📌PDF:大白话说Java面试题 — 06_Spring篇

第14题:Spring 支持的 Bean 作用域

📚回答:

  • 核心考点: Spring Bean 作用域是 Spring IoC 容器的核心设计之一,大厂面试不会只问"有哪几种",而是深入考察各作用域的底层实现机制DefaultListableBeanFactory如何管理不同作用域的 Bean)、作用域代理ScopedProxyMode的工作原理(CGLIB/JDK 代理在跨作用域注入时的角色)、Web 作用域与RequestContextListener/ServletRequestListener的生命周期绑定以及prototype作用域 Bean 的销毁机制与内存泄漏风险。面试官真正想判断的是:你是否能从源码层面理解作用域的设计意图,以及能否在 Web 应用、微服务、分布式会话等生产场景中正确选型。

1. Spring 支持的六种 Bean 作用域

Spring Framework 定义了 6 种标准作用域,其中 2 种适用于所有应用,4 种仅适用于 Web 环境:

作用域常量说明生命周期线程安全适用场景
singletonConfigurableBeanFactory.SCOPE_SINGLETON每个 Spring 容器只有一个实例容器启动时创建,容器关闭时销毁无状态安全,有状态不安全Service、DAO、配置类
prototypeConfigurableBeanFactory.SCOPE_PROTOTYPE每次获取都创建新实例获取时创建,Spring 不管理销毁安全(实例隔离)有状态对象、多例策略
requestWebApplicationContext.SCOPE_REQUEST每个 HTTP 请求一个实例请求开始时创建,请求结束时销毁安全(请求隔离)请求级上下文、TraceId
sessionWebApplicationContext.SCOPE_SESSION每个 HTTP Session 一个实例Session 创建时创建,Session 失效时销毁安全(会话隔离)用户购物车、登录状态
applicationWebApplicationContext.SCOPE_APPLICATION每个 ServletContext 一个实例应用启动时创建,应用关闭时销毁无状态安全,有状态不安全全局配置、应用级缓存
websocketWebApplicationContext.SCOPE_WEBSOCKET每个 WebSocket 连接一个实例连接建立时创建,连接关闭时销毁安全(连接隔离)WebSocket 会话状态

注意global-session(全局会话)在 Spring 5 中已随 Portlet 支持一起移除,不再推荐使用。


2. singleton 作用域——默认且最常用
  • 2.1 定义与实现

    singleton是 Spring 的默认作用域,每个 Spring 容器只创建一个 Bean 实例,存储在DefaultSingletonBeanRegistry.singletonObjects(一级缓存)中。

    // 默认就是 singleton,可省略 @Scope@ComponentpublicclassUserService{}// 显式声明@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)@ComponentpublicclassUserService{}
  • 2.2 单例 Bean 的创建时机

    配置创建时机说明
    默认(非懒加载)容器启动时ApplicationContext.refresh()阶段
    @Lazy首次获取时getBean()或依赖注入时
    lazy-init="true"(XML)首次获取时@Lazy
    @Lazy// 延迟初始化@ComponentpublicclassHeavyService{}
  • 2.3 单例 Bean 的线程安全

    单例 Bean 被多线程共享,必须设计为无状态

    @Service// singleton,无状态,线程安全publicclassUserService{@AutowiredprivateUserDaouserDao;// 依赖注入,本身无状态publicUsergetUser(Longid){returnuserDao.findById(id);// 纯查询,不修改实例变量}}

3. prototype 作用域——每次获取新实例
  • 3.1 定义与实现

    每次调用getBean()或注入依赖时,Spring 都会创建一个新的 Bean 实例。

    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ComponentpublicclassPrototypeBean{privateintcount=0;publicvoidincrement(){count++;}publicintgetCount(){returncount;}}
    @AutowiredprivatePrototypeBeanbean1;@AutowiredprivatePrototypeBeanbean2;// bean1 != bean2,是两个不同的实例
  • 3.2 prototype 的销毁机制——重大陷阱!

    Spring 不管理 prototype Bean 的完整生命周期。虽然会调用初始化回调(@PostConstructInitializingBean),但不会调用销毁回调@PreDestroyDisposableBean)。

    @Scope("prototype")@ComponentpublicclassPrototypeResourceimplementsDisposableBean{privateConnectionconnection;@PostConstructpublicvoidinit(){connection=dataSource.getConnection();// 获取资源}@Overridepublicvoiddestroy(){connection.close();// ❌ Spring 不会调用!内存泄漏!}}

    解决方案

    方案实现方式说明
    自定义销毁逻辑客户端代码手动调用销毁侵入性强,不推荐
    Bean 后处理器实现DestructionAwareBeanPostProcessor在 Bean 销毁前执行清理
    使用 ObjectFactory延迟获取,客户端管理生命周期推荐
    // 推荐:使用 ObjectFactory,客户端控制生命周期@ServicepublicclassClientService{@AutowiredprivateObjectFactory<PrototypeResource>resourceFactory;publicvoiddoWork(){PrototypeResourceresource=resourceFactory.getObject();try{// 使用资源...}finally{resource.close();// 客户端负责清理}}}
  • 3.3 prototype 的性能考量

    频繁创建 prototype Bean 可能带来性能问题:

    场景影响优化方案
    每次请求创建对象创建开销大使用对象池(Apache Commons Pool)
    依赖注入复杂依赖树递归创建使用ObjectFactory延迟创建
    内存占用高大量实例未回收确保客户端及时释放

4. Web 作用域——request、session、application、websocket

Web 作用域仅在 Web 应用上下文中有效(WebApplicationContext),需要配置RequestContextListenerDispatcherServlet

  • 4.1 request 作用域

    每个 HTTP 请求创建一个实例,请求结束后销毁。

    @Scope(value=WebApplicationContext.SCOPE_REQUEST,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassRequestContext{privateStringtraceId;privateLonguserId;// ... 请求级状态}

    底层绑定机制:Spring 通过RequestContextListener监听请求生命周期,在requestInitialized()时创建 Bean,在requestDestroyed()时销毁。

  • 4.2 session 作用域

    每个 HTTP Session 创建一个实例,Session 失效时销毁。

    @Scope(value=WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassShoppingCart{privateList<Item>items=newArrayList<>();// ... 购物车状态}

    分布式 Session 问题:在微服务/集群环境下,Session 默认不共享。解决方案:

    1. Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享;
    2. JWT Token:无状态认证,不依赖 Session;
    3. Sticky Session:负载均衡器将同一用户固定到同一节点(不推荐)。
  • 4.3 application 作用域

    每个 ServletContext 创建一个实例,等同于singleton,但生命周期绑定到 Web 应用。

    @Scope(value=WebApplicationContext.SCOPE_APPLICATION,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassAppConfig{privateMap<String,Object>globalCache=newConcurrentHashMap<>();}
  • 4.4 websocket 作用域

    每个 WebSocket 连接创建一个实例,连接关闭时销毁。

    @Scope(scopeName="websocket",proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassWebSocketSession{privateStringsessionId;privateList<Message>messages=newArrayList<>();}

5. 作用域代理 ScopedProxyMode——跨作用域注入的核心
  • 5.1 为什么需要作用域代理?

    singletonBean 注入request/session/prototype作用域 Bean 时,由于singletonBean 只创建一次,注入的依赖在首次注入后固定不变,导致后续请求获取的是旧实例。

    @Service// singletonpublicclassUserService{@AutowiredprivateRequestContextrequestContext;// request 作用域publicvoidprocess(){// 问题:requestContext 是第一次注入时的实例,不是当前请求的!StringtraceId=requestContext.getTraceId();}}
  • 5.2 ScopedProxyMode 的工作原理

    ScopedProxyMode为作用域 Bean 创建代理对象singletonBean 注入的是代理而非真实实例。每次调用代理方法时,代理从当前作用域获取真实实例。

    代理模式实现方式适用条件说明
    NO不创建代理同作用域注入默认,无代理开销
    INTERFACESJDK 动态代理目标类实现接口要求目标类有接口
    TARGET_CLASSCGLIB 代理任意类最常用,无需接口
    @Scope(value=WebApplicationContext.SCOPE_REQUEST,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassRequestContext{}

    代理执行流程

    UserService(singleton)调用 requestContext.getTraceId() ↓ 调用 CGLIB 代理对象的 getTraceId() ↓ 代理从 RequestAttributes(ThreadLocal)获取当前 Request ↓ 从 Request 作用域缓存中获取真实的 RequestContext 实例 ↓ 调用真实实例的 getTraceId()
  • 5.3 prototype 作用域的代理问题

    即使配置了proxyMode = TARGET_CLASSsingletonBean 注入prototypeBean 时,由于代理对象本身也是单例的,每次调用代理方法时虽然会创建新的目标实例,但如果代理方法内部缓存了引用,仍然可能共享状态。

    @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassPrototypeBean{}@ServicepublicclassUserService{@AutowiredprivatePrototypeBeanprototypeBean;publicvoidprocess(){// 每次调用都会创建新的 PrototypeBean 实例prototypeBean.doSomething();}}

    更推荐的方式:使用ObjectFactory@Lookup方法:

    @ServicepublicclassUserService{@AutowiredprivateObjectFactory<PrototypeBean>prototypeBeanFactory;publicvoidprocess(){PrototypeBeanbean=prototypeBeanFactory.getObject();// 每次获取新实例bean.doSomething();}}// 或使用 @Lookup@ServicepublicabstractclassUserService{@LookupprotectedabstractPrototypeBeangetPrototypeBean();publicvoidprocess(){PrototypeBeanbean=getPrototypeBean();// 每次获取新实例bean.doSomething();}}

6. 自定义作用域

Spring 允许自定义作用域,实现Scope接口:

// 自定义线程作用域publicclassThreadScopeimplementsScope{privatefinalThreadLocal<Map<String,Object>>threadScope=ThreadLocal.withInitial(HashMap::new);@OverridepublicObjectget(Stringname,ObjectFactory<?>objectFactory){Map<String,Object>scope=threadScope.get();Objectbean=scope.get(name);if(bean==null){bean=objectFactory.getObject();scope.put(name,bean);}returnbean;}@OverridepublicObjectremove(Stringname){returnthreadScope.get().remove(name);}// ... 其他方法}// 注册自定义作用域@BeanpublicCustomScopeConfigurercustomScopeConfigurer(){CustomScopeConfigurerconfigurer=newCustomScopeConfigurer();configurer.addScope("thread",newThreadScope());returnconfigurer;}// 使用@Scope("thread")@ComponentpublicclassThreadScopedBean{}

7. 生产环境避坑指南
  • 7.1 prototype Bean 的内存泄漏

    Spring 不管理 prototype Bean 的销毁,如果 Bean 持有资源(数据库连接、线程池),必须客户端手动释放。

  • 7.2 session 作用域在分布式环境失效

    集群环境下 Session 默认不共享,使用 Spring Session + Redis 或 JWT 替代。

  • 7.3 忘记配置 proxyMode 导致数据串乱

    @Scope("request")// ❌ 忘记 proxyMode@ComponentpublicclassRequestContext{}// singleton Bean 注入后,所有请求共享同一个 RequestContext 实例!
  • 7.4 Web 作用域在非 Web 环境启动失败

    request/session等作用域需要WebApplicationContext,在纯 Java 应用中使用会抛出IllegalStateException

  • 7.5 @Async 与作用域 Bean

    @Async使用线程池执行异步任务,脱离了原请求/会话的上下文,作用域 Bean 可能获取不到正确的实例。


8. 面试官追问与高分回答模板
  • 追问 1:“Spring 支持哪些 Bean 作用域?”

    低分回答:“有 singleton、prototype、request、session、global-session 五种。”(过时,缺少 application 和 websocket)

    高分回答

    "Spring 支持 6 种标准作用域:

    作用域适用范围说明
    singleton所有应用每个容器一个实例,默认作用域
    prototype所有应用每次获取创建新实例
    requestWeb 应用每个 HTTP 请求一个实例
    sessionWeb 应用每个 HTTP Session 一个实例
    applicationWeb 应用每个 ServletContext 一个实例
    websocketWeb 应用每个 WebSocket 连接一个实例

    注意global-session在 Spring 5 中已随 Portlet 支持移除。其中singletonprototype适用于所有应用类型,其余 4 种需要 Web 环境。"

  • 追问 2:“singleton 和 prototype 有什么区别?prototype 有什么陷阱?”

    高分回答

    "| 维度 | singleton | prototype |
    |------|-----------|-----------|
    |实例数量| 每个容器一个 | 每次获取创建新实例 |
    |创建时机| 容器启动(默认)或首次获取(@Lazy) | 每次 getBean() 或注入时 |
    |销毁管理| Spring 管理完整生命周期 |Spring 不调用销毁回调!|
    |线程安全| 需设计为无状态 | 天然安全(实例隔离) |
    |性能| 创建一次,复用 | 频繁创建,开销大 |

    prototype 的最大陷阱是销毁机制:Spring 会调用@PostConstruct/InitializingBean初始化,但不会调用@PreDestroy/DisposableBean销毁。如果 prototype Bean 持有数据库连接、线程池等资源,会导致内存泄漏。

    解决方案:使用ObjectFactory延迟获取,由客户端管理生命周期;或实现自定义的DestructionAwareBeanPostProcessor。"

  • 追问 3:“request 作用域 Bean 怎么在 singleton Bean 中使用?”

    高分回答

    "直接在 singleton Bean 中注入 request 作用域 Bean 会有问题:singleton 只创建一次,注入的 request Bean 在首次注入后固定不变,后续请求获取的是旧实例。

    解决方案是配置proxyMode = ScopedProxyMode.TARGET_CLASS

    @Scope(value=WebApplicationContext.SCOPE_REQUEST,proxyMode=ScopedProxyMode.TARGET_CLASS)@ComponentpublicclassRequestContext{}

    原理:Spring 为 request Bean 创建 CGLIB 代理对象,singleton Bean 注入的是代理。每次调用代理方法时,代理从当前 Request 作用域(通过RequestContextHolder,底层 ThreadLocal)获取真实的 Bean 实例,确保每次请求获取的都是当前请求的实例。

    类似地,session、application、websocket 作用域跨域注入时也需要配置 proxyMode。"

  • 追问 4:“Spring 的 session 作用域在分布式环境下有什么问题?”

    高分回答

    "Spring 的session作用域基于 Servlet 容器的 HttpSession,在分布式/集群环境下存在 Session 不共享的问题:

    1. Session 粘滞(Sticky Session):负载均衡器将同一用户固定到同一节点,但节点故障时会话丢失;
    2. Session 复制:Tomcat 等容器支持 Session 复制,但性能开销大,不适合大规模集群;
    3. Session 共享:使用 Redis/Memcached 等外部存储共享 Session。

    推荐方案:

    • Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享,同时支持session作用域 Bean 的跨节点一致性;
    • JWT Token:无状态认证,不依赖服务器 Session,天然支持分布式;
    • OAuth2/OIDC:使用令牌机制,服务端无会话状态。

    现代微服务架构中,推荐使用 JWT 等无状态方案,彻底避免 Session 共享问题。"

  • 追问 5:“prototype 作用域 Bean 的销毁机制是怎样的?”

    高分回答

    "Spring不管理 prototype Bean 的销毁。具体表现:

    1. 初始化回调会执行@PostConstructInitializingBean.afterPropertiesSet()、自定义 init-method 都会正常调用;
    2. 销毁回调不会执行@PreDestroyDisposableBean.destroy()、自定义 destroy-method不会被 Spring 调用

    原因:Spring 的DefaultSingletonBeanRegistry只管理 singleton Bean 的销毁,prototype Bean 创建后直接返回给客户端,Spring 不持有引用,因此无法在容器关闭时遍历销毁。

    解决方案

    1. 使用 ObjectFactory:客户端通过ObjectFactory.getObject()获取实例,使用完毕后手动调用清理方法;
    2. 自定义 Scope 实现:在自定义 Scope 中管理 Bean 的生命周期;
    3. DestructionAwareBeanPostProcessor:实现该接口,在postProcessBeforeDestruction()中处理 prototype Bean 的清理。

    最佳实践:避免在 prototype Bean 中持有需要释放的资源,或确保客户端代码负责资源清理。"

  • 追问 6:“Spring 允许自定义作用域吗?怎么实现?”

    高分回答

    "Spring 允许自定义作用域,需要实现org.springframework.beans.factory.config.Scope接口:

    publicclassThreadScopeimplementsScope{privatefinalThreadLocal<Map<String,Object>>threadScope=ThreadLocal.withInitial(HashMap::new);@OverridepublicObjectget(Stringname,ObjectFactory<?>objectFactory){Map<String,Object>scope=threadScope.get();returnscope.computeIfAbsent(name,k->objectFactory.getObject());}@OverridepublicObjectremove(Stringname){returnthreadScope.get().remove(name);}// ... 实现 registerDestructionCallback、resolveContextualObject、getConversationId}

    然后通过CustomScopeConfigurer注册:

    @BeanpublicCustomScopeConfigurercustomScopeConfigurer(){CustomScopeConfigurerconfigurer=newCustomScopeConfigurer();configurer.addScope("thread",newThreadScope());returnconfigurer;}

    典型应用场景:实现线程级作用域(每个线程一个实例),用于线程池环境下的状态隔离。"


9. 方案选型速查表
业务场景推荐作用域代理模式核心理由
Service/DAO 层singletonNO无状态设计,性能最优
有状态策略对象prototypeNO/ObjectFactory每次获取新实例,客户端管理生命周期
请求级上下文(TraceId)requestTARGET_CLASS请求隔离,跨 singleton 注入需代理
用户购物车sessionTARGET_CLASS会话隔离,注意分布式 Session
全局配置/缓存applicationTARGET_CLASS应用级共享
WebSocket 会话websocketTARGET_CLASS连接隔离
线程级状态隔离自定义threadNO线程池环境下隔离状态
延迟初始化singleton+@LazyNO优化启动速度

💡面试官想要的满分总结

Spring 的 6 种 Bean 作用域是 IoC 容器管理对象生命周期和可见范围的核心机制。理解作用域必须抓住三个关键点:

  1. singleton 是默认且最常用:必须设计为无状态,利用容器启动时创建、全局复用的特性提升性能。@Lazy可优化启动速度。
  2. prototype 的销毁陷阱:Spring 不管理 prototype Bean 的销毁,如果持有资源(连接、线程池)会导致内存泄漏。推荐使用ObjectFactory由客户端管理生命周期。
  3. Web 作用域必须配代理request/session等作用域 Bean 被 singleton Bean 注入时,必须配置proxyMode = TARGET_CLASS,通过 CGLIB 代理确保每次调用获取当前作用域的真实实例。

工程实践中,99% 的 Bean 使用 singleton + 无状态设计。Web 作用域(request/session)适用于请求级/会话级上下文,但需注意分布式环境下的 Session 共享问题。自定义作用域(如线程级作用域)在特定场景下(线程池状态隔离)有独特价值。理解ScopedProxyMode的代理机制——从 ThreadLocal 获取当前作用域实例——是掌握 Web 作用域的核心。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯