📌 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(一级缓存)中。java// 默认就是 singleton,可省略 @Scope @Component public class UserService { } // 显式声明 @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) @Component public class UserService { } -
2.2 单例 Bean 的创建时机
配置 创建时机 说明 默认(非懒加载) 容器启动时 ApplicationContext.refresh()阶段@Lazy首次获取时 getBean()或依赖注入时lazy-init="true"(XML)首次获取时 同 @Lazyjava@Lazy // 延迟初始化 @Component public class HeavyService { } -
2.3 单例 Bean 的线程安全
单例 Bean 被多线程共享,必须设计为无状态:
java@Service // singleton,无状态,线程安全 public class UserService { @Autowired private UserDao userDao; // 依赖注入,本身无状态 public User getUser(Long id) { return userDao.findById(id); // 纯查询,不修改实例变量 } }
3. prototype 作用域------每次获取新实例
-
3.1 定义与实现
每次调用
getBean()或注入依赖时,Spring 都会创建一个新的 Bean 实例。java@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Component public class PrototypeBean { private int count = 0; public void increment() { count++; } public int getCount() { return count; } }java@Autowired private PrototypeBean bean1; @Autowired private PrototypeBean bean2; // bean1 != bean2,是两个不同的实例 -
3.2 prototype 的销毁机制------重大陷阱!
Spring 不管理 prototype Bean 的完整生命周期 。虽然会调用初始化回调(
@PostConstruct、InitializingBean),但不会调用销毁回调 (@PreDestroy、DisposableBean)。java@Scope("prototype") @Component public class PrototypeResource implements DisposableBean { private Connection connection; @PostConstruct public void init() { connection = dataSource.getConnection(); // 获取资源 } @Override public void destroy() { connection.close(); // ❌ Spring 不会调用!内存泄漏! } }解决方案:
方案 实现方式 说明 自定义销毁逻辑 客户端代码手动调用销毁 侵入性强,不推荐 Bean 后处理器 实现 DestructionAwareBeanPostProcessor在 Bean 销毁前执行清理 使用 ObjectFactory 延迟获取,客户端管理生命周期 推荐 java// 推荐:使用 ObjectFactory,客户端控制生命周期 @Service public class ClientService { @Autowired private ObjectFactory<PrototypeResource> resourceFactory; public void doWork() { PrototypeResource resource = 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 请求创建一个实例,请求结束后销毁。
java@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class RequestContext { private String traceId; private Long userId; // ... 请求级状态 }底层绑定机制 :Spring 通过
RequestContextListener监听请求生命周期,在requestInitialized()时创建 Bean,在requestDestroyed()时销毁。 -
4.2 session 作用域
每个 HTTP Session 创建一个实例,Session 失效时销毁。
java@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class ShoppingCart { private List<Item> items = new ArrayList<>(); // ... 购物车状态 }分布式 Session 问题:在微服务/集群环境下,Session 默认不共享。解决方案:
- Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享;
- JWT Token:无状态认证,不依赖 Session;
- Sticky Session:负载均衡器将同一用户固定到同一节点(不推荐)。
-
4.3 application 作用域
每个 ServletContext 创建一个实例,等同于
singleton,但生命周期绑定到 Web 应用。java@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class AppConfig { private Map<String, Object> globalCache = new ConcurrentHashMap<>(); } -
4.4 websocket 作用域
每个 WebSocket 连接创建一个实例,连接关闭时销毁。
java@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class WebSocketSession { private String sessionId; private List<Message> messages = new ArrayList<>(); }
5. 作用域代理 ScopedProxyMode------跨作用域注入的核心
-
5.1 为什么需要作用域代理?
当
singletonBean 注入request/session/prototype作用域 Bean 时,由于singletonBean 只创建一次,注入的依赖在首次注入后固定不变,导致后续请求获取的是旧实例。java@Service // singleton public class UserService { @Autowired private RequestContext requestContext; // request 作用域 public void process() { // 问题:requestContext 是第一次注入时的实例,不是当前请求的! String traceId = requestContext.getTraceId(); } } -
5.2 ScopedProxyMode 的工作原理
ScopedProxyMode为作用域 Bean 创建代理对象 ,singletonBean 注入的是代理而非真实实例。每次调用代理方法时,代理从当前作用域获取真实实例。代理模式 实现方式 适用条件 说明 NO不创建代理 同作用域注入 默认,无代理开销 INTERFACESJDK 动态代理 目标类实现接口 要求目标类有接口 TARGET_CLASSCGLIB 代理 任意类 最常用,无需接口 java@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class RequestContext { }代理执行流程:
UserService(singleton)调用 requestContext.getTraceId() ↓ 调用 CGLIB 代理对象的 getTraceId() ↓ 代理从 RequestAttributes(ThreadLocal)获取当前 Request ↓ 从 Request 作用域缓存中获取真实的 RequestContext 实例 ↓ 调用真实实例的 getTraceId() -
5.3 prototype 作用域的代理问题
即使配置了
proxyMode = TARGET_CLASS,singletonBean 注入prototypeBean 时,由于代理对象本身也是单例的,每次调用代理方法时虽然会创建新的目标实例,但如果代理方法内部缓存了引用,仍然可能共享状态。java@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class PrototypeBean { } @Service public class UserService { @Autowired private PrototypeBean prototypeBean; public void process() { // 每次调用都会创建新的 PrototypeBean 实例 prototypeBean.doSomething(); } }更推荐的方式 :使用
ObjectFactory或@Lookup方法:java@Service public class UserService { @Autowired private ObjectFactory<PrototypeBean> prototypeBeanFactory; public void process() { PrototypeBean bean = prototypeBeanFactory.getObject(); // 每次获取新实例 bean.doSomething(); } } // 或使用 @Lookup @Service public abstract class UserService { @Lookup protected abstract PrototypeBean getPrototypeBean(); public void process() { PrototypeBean bean = getPrototypeBean(); // 每次获取新实例 bean.doSomething(); } }
6. 自定义作用域
Spring 允许自定义作用域,实现 Scope 接口:
java
// 自定义线程作用域
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadScope =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadScope.get();
Object bean = scope.get(name);
if (bean == null) {
bean = objectFactory.getObject();
scope.put(name, bean);
}
return bean;
}
@Override
public Object remove(String name) {
return threadScope.get().remove(name);
}
// ... 其他方法
}
// 注册自定义作用域
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new ThreadScope());
return configurer;
}
// 使用
@Scope("thread")
@Component
public class ThreadScopedBean { }
7. 生产环境避坑指南
-
7.1 prototype Bean 的内存泄漏
Spring 不管理 prototype Bean 的销毁,如果 Bean 持有资源(数据库连接、线程池),必须客户端手动释放。
-
7.2 session 作用域在分布式环境失效
集群环境下 Session 默认不共享,使用 Spring Session + Redis 或 JWT 替代。
-
7.3 忘记配置 proxyMode 导致数据串乱
java@Scope("request") // ❌ 忘记 proxyMode @Component public class RequestContext { } // 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:java@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class RequestContext { }原理: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接口:javapublic class ThreadScope implements Scope { private final ThreadLocal<Map<String, Object>> threadScope = ThreadLocal.withInitial(HashMap::new); @Override public Object get(String name, ObjectFactory<?> objectFactory) { Map<String, Object> scope = threadScope.get(); return scope.computeIfAbsent(name, k -> objectFactory.getObject()); } @Override public Object remove(String name) { return threadScope.get().remove(name); } // ... 实现 registerDestructionCallback、resolveContextualObject、getConversationId }然后通过
CustomScopeConfigurer注册:java@Bean public CustomScopeConfigurer customScopeConfigurer() { CustomScopeConfigurer configurer = new CustomScopeConfigurer(); configurer.addScope("thread", new ThreadScope()); return configurer; }典型应用场景:实现线程级作用域(每个线程一个实例),用于线程池环境下的状态隔离。"
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 作用域的核心。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯