【大白话说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 环境:
| 作用域 | 常量 | 说明 | 生命周期 | 线程安全 | 适用场景 |
|---|---|---|---|---|---|
singleton | ConfigurableBeanFactory.SCOPE_SINGLETON | 每个 Spring 容器只有一个实例 | 容器启动时创建,容器关闭时销毁 | 无状态安全,有状态不安全 | Service、DAO、配置类 |
prototype | ConfigurableBeanFactory.SCOPE_PROTOTYPE | 每次获取都创建新实例 | 获取时创建,Spring 不管理销毁 | 安全(实例隔离) | 有状态对象、多例策略 |
request | WebApplicationContext.SCOPE_REQUEST | 每个 HTTP 请求一个实例 | 请求开始时创建,请求结束时销毁 | 安全(请求隔离) | 请求级上下文、TraceId |
session | WebApplicationContext.SCOPE_SESSION | 每个 HTTP Session 一个实例 | Session 创建时创建,Session 失效时销毁 | 安全(会话隔离) | 用户购物车、登录状态 |
application | WebApplicationContext.SCOPE_APPLICATION | 每个 ServletContext 一个实例 | 应用启动时创建,应用关闭时销毁 | 无状态安全,有状态不安全 | 全局配置、应用级缓存 |
websocket | WebApplicationContext.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 的完整生命周期。虽然会调用初始化回调(
@PostConstruct、InitializingBean),但不会调用销毁回调(@PreDestroy、DisposableBean)。@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),需要配置RequestContextListener或DispatcherServlet。
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 默认不共享。解决方案:
- Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享;
- JWT Token:无状态认证,不依赖 Session;
- 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_CLASS,singletonBean 注入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 支持移除。其中singleton和prototype适用于所有应用类型,其余 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 不共享的问题:- Session 粘滞(Sticky Session):负载均衡器将同一用户固定到同一节点,但节点故障时会话丢失;
- Session 复制:Tomcat 等容器支持 Session 复制,但性能开销大,不适合大规模集群;
- Session 共享:使用 Redis/Memcached 等外部存储共享 Session。
推荐方案:
- Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享,同时支持
session作用域 Bean 的跨节点一致性; - JWT Token:无状态认证,不依赖服务器 Session,天然支持分布式;
- OAuth2/OIDC:使用令牌机制,服务端无会话状态。
现代微服务架构中,推荐使用 JWT 等无状态方案,彻底避免 Session 共享问题。"
追问 5:“prototype 作用域 Bean 的销毁机制是怎样的?”
高分回答:
"Spring不管理 prototype Bean 的销毁。具体表现:
- 初始化回调会执行:
@PostConstruct、InitializingBean.afterPropertiesSet()、自定义 init-method 都会正常调用; - 销毁回调不会执行:
@PreDestroy、DisposableBean.destroy()、自定义 destroy-method不会被 Spring 调用。
原因:Spring 的
DefaultSingletonBeanRegistry只管理 singleton Bean 的销毁,prototype Bean 创建后直接返回给客户端,Spring 不持有引用,因此无法在容器关闭时遍历销毁。解决方案:
- 使用 ObjectFactory:客户端通过
ObjectFactory.getObject()获取实例,使用完毕后手动调用清理方法; - 自定义 Scope 实现:在自定义 Scope 中管理 Bean 的生命周期;
- 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 层 | singleton | NO | 无状态设计,性能最优 |
| 有状态策略对象 | prototype | NO/ObjectFactory | 每次获取新实例,客户端管理生命周期 |
| 请求级上下文(TraceId) | request | TARGET_CLASS | 请求隔离,跨 singleton 注入需代理 |
| 用户购物车 | session | TARGET_CLASS | 会话隔离,注意分布式 Session |
| 全局配置/缓存 | application | TARGET_CLASS | 应用级共享 |
| WebSocket 会话 | websocket | TARGET_CLASS | 连接隔离 |
| 线程级状态隔离 | 自定义thread | NO | 线程池环境下隔离状态 |
| 延迟初始化 | singleton+@Lazy | NO | 优化启动速度 |
💡面试官想要的满分总结:
Spring 的 6 种 Bean 作用域是 IoC 容器管理对象生命周期和可见范围的核心机制。理解作用域必须抓住三个关键点:
- singleton 是默认且最常用:必须设计为无状态,利用容器启动时创建、全局复用的特性提升性能。
@Lazy可优化启动速度。- prototype 的销毁陷阱:Spring 不管理 prototype Bean 的销毁,如果持有资源(连接、线程池)会导致内存泄漏。推荐使用
ObjectFactory由客户端管理生命周期。- Web 作用域必须配代理:
request/session等作用域 Bean 被 singleton Bean 注入时,必须配置proxyMode = TARGET_CLASS,通过 CGLIB 代理确保每次调用获取当前作用域的真实实例。工程实践中,99% 的 Bean 使用 singleton + 无状态设计。Web 作用域(request/session)适用于请求级/会话级上下文,但需注意分布式环境下的 Session 共享问题。自定义作用域(如线程级作用域)在特定场景下(线程池状态隔离)有独特价值。理解
ScopedProxyMode的代理机制——从 ThreadLocal 获取当前作用域实例——是掌握 Web 作用域的核心。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯
