前言:什么是Bean的作用域?
在Spring IoC容器中,Bean的作用域(Scope)决定了Bean实例的生命周期、可见性以及创建方式。简单来说,当你向Spring容器请求一个Bean时,容器是返回一个全新的实例,还是返回一个共享的缓存实例?答案由作用域决定。
理解作用域不仅仅是记住几个注解,它关系到线程安全、状态管理、性能优化以及架构设计。本文将深入剖析Spring内置的六大作用域,并带你从源码层面理解其实现机制,最终手写一个自定义作用域(如TenantContext)。
第一部分:基础篇——六种内置作用域详解
Spring(包括Spring Boot)主要支持以下六种作用域。其中前两种是Spring核心容器的基础,后四种通常只在WebApplicationContext中有效。
1. Singleton(单例)
定义:在整个Spring IoC容器中,只存在一个Bean实例。所有对该Bean的请求,只要id或name匹配,都返回同一个对象引用。
特点:
默认作用域:如果不显式指定,所有Bean都是Singleton。
生命周期:容器启动时实例化(默认情况下,也可配置懒加载),容器关闭时销毁。
线程安全:开发者必须自行处理。由于实例是共享的,如果Bean中有可变状态(如实例变量),在多线程环境下会出现并发问题。
适用场景:无状态的服务层Bean(Service)、DAO层、配置类、工具类。
配置方式:
java
// 注解方式 @Component @Scope("singleton") // 或者不写,默认就是singleton public class UserService {} // XML方式 <bean id="userService" class="com.example.UserService" scope="singleton"/>2. Prototype(原型)
定义:每次对该Bean的请求(如调用getBean或依赖注入),容器都会创建一个全新的实例。
特点:
生命周期:容器只负责创建和初始化,不负责销毁。创建后,容器不再持有该实例的引用,销毁方法(如
@PreDestroy)通常不会被调用。性能开销:每次获取都涉及对象的创建和依赖注入,高频创建时需考虑GC压力。
状态管理:适合持有私有状态的对象,每个调用者拥有独立副本。
适用场景:有状态的对象(如每次请求携带不同用户数据的Model)、多线程环境下的非线程安全对象。
重要陷阱:
如果在Singleton Bean中注入Prototype Bean,由于Singleton只实例化一次,注入过程也只发生一次,因此Prototype Bean会退化为单例行为。
解决方案:
方法注入(Method Injection):使用
@Lookup注解。作用域代理(Scoped Proxy):结合
proxyMode。
代码示例:
java
@Component @Scope(value = "prototype") public class ShoppingCart { private List<Item> items = new ArrayList<>(); // ... } @Component public class OrderService { // 每次调用该方法都会获取一个新的ShoppingCart实例 @Lookup public ShoppingCart getShoppingCart() { return null; // Spring会动态重写此方法 } }3. Request(请求)
定义:仅适用于Web应用。每个HTTP请求会创建一个全新的Bean实例,该实例仅在当前请求的生命周期内有效(从请求进入DispatcherServlet到响应返回)。
特点:
隔离性:不同请求之间的Bean完全隔离,适合存储请求上下文数据(如认证信息、表单数据)。
实现原理:基于
RequestContextHolder和ThreadLocal。Spring会将当前请求绑定到线程,通过AOP代理暴露Bean。销毁:请求结束时,Bean自动销毁。
适用场景:需要持有请求特定数据的Controller、Interceptor辅助类。
4. Session(会话)
定义:每个HTTP Session会创建一个Bean实例。该实例在整个会话期间(用户浏览器打开到关闭)都是同一个实例。
特点:
生命周期:用户会话建立时创建(或懒加载),会话过期时销毁。
内存风险:如果Session作用域的Bean持有大对象或数量过多,易导致内存泄漏或Session膨胀。
适用场景:用户登录信息、购物车、用户偏好设置。
5. Application(应用)
定义:在ServletContext生命周期内,只存在一个Bean实例。类似于Singleton,但作用域范围是ServletContext(即整个Web应用),而非Spring IoC容器。
特点:
区别:如果一个应用有多个Spring容器(如父子容器),Application作用域的Bean在所有容器中共享一份;而Singleton在各自容器中独立。
生命周期:随Web应用启动而创建,关闭而销毁。
6. WebSocket(WebSocket会话)
定义:每个WebSocket会话生命周期内,存在一个Bean实例。
特点:
粒度:介于Session和Request之间,长连接场景。
适用场景:WebSocket会话状态的维护。
第二部分:进阶篇——源码深度剖析
要真正理解作用域,必须深入Spring容器管理的核心——BeanFactory和Scope接口。
1. Spring如何解析Bean的作用域?
在Spring中,Bean定义(BeanDefinition)包含一个scope属性。当调用getBean时,AbstractBeanFactory的doGetBean方法会检查该属性:
java
// AbstractBeanFactory.java (简化) protected <T> T doGetBean(...) { // 1. 如果是单例,尝试从缓存中获取 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { return createBean(beanName, mbd, args); }); return (T) getObjectForBeanInstance(sharedInstance, ...); } // 2. 如果是原型,直接创建 else if (mbd.isPrototype()) { return (T) createBean(beanName, mbd, args); } // 3. 其他作用域(request, session等) else { String scopeName = mbd.getScope(); Scope scope = this.scopes.get(scopeName); // 委托给特定的Scope实现类 Object scopedInstance = scope.get(beanName, () -> { return createBean(beanName, mbd, args); }); return (T) getObjectForBeanInstance(scopedInstance, ...); } }2. 核心接口:org.springframework.beans.factory.config.Scope
所有自定义作用域都必须实现该接口。它定义了四个核心方法:
Object get(String name, ObjectFactory<?> objectFactory):获取Bean。如果当前作用域中不存在,则通过objectFactory创建。Object remove(String name):移除Bean。void registerDestructionCallback(String name, Runnable callback):注册销毁回调。Object resolveContextualObject(String key):解析上下文对象(如Request作用域中的#request变量)。
3. Web作用域的实现原理(以RequestScope为例)
Spring Web模块中的RequestScope实现了Scope接口。它的核心是利用了RequestContextHolder获取当前线程绑定的ServletRequestAttributes。
java
// RequestScope.java 核心逻辑简化 public Object get(String name, ObjectFactory<?> objectFactory) { // 获取当前请求的属性Map Map<String, Object> attributes = getRequestAttributes(); // 尝试从请求域中获取 Object scopedObject = attributes.get(name); if (scopedObject == null) { // 不存在则创建 scopedObject = objectFactory.getObject(); attributes.put(name, scopedObject); } return scopedObject; }关键点:
线程隔离:
RequestContextHolder内部使用ThreadLocal存储当前请求的RequestAttributes。代理模式:当我们将一个Request作用域的Bean注入到Singleton的Controller时,Spring不会直接注入该Bean,而是注入一个AOP代理(Scoped Proxy)。每次调用代理的方法时,代理会从
ThreadLocal中取出当前请求对应的真实Bean来执行。
第三部分:实战篇——作用域代理详解
1. 为什么需要作用域代理?
假设一个ShoppingCart是Session作用域,它被注入到一个Singleton的OrderService中。由于OrderService只实例化一次,注入只会发生一次,此时注入的ShoppingCart是哪个Session的?这显然是错误的。
Spring通过作用域代理解决此问题:我们不直接注入ShoppingCart实例,而是注入一个代理对象。这个代理对象在运行时,每次调用方法时,都会去当前作用域(如当前Session)中查找真正的实例。
2. 开启作用域代理
方式一:XML配置
xml
<bean id="shoppingCart" class="com.example.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="true"/> </bean>
方式二:注解配置
java
@Component @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) public class ShoppingCart {}ScopedProxyMode.INTERFACES:基于JDK动态代理。ScopedProxyMode.TARGET_CLASS:基于CGLIB(推荐,即使有接口也能工作)。
3. 代理对象的生成机制
当容器检测到proxyMode时,它不会注册原始Bean,而是注册一个ScopedProxyFactoryBean。该工厂Bean生成的代理对象内部持有一个BeanFactory引用,每次方法调用都会通过getBean去Scope中获取真实对象。
java
// 代理逻辑伪代码 public class ShoppingCartProxy extends ShoppingCart { private BeanFactory beanFactory; private String beanName; @Override public void addItem(Item item) { // 每次调用都去Scope中获取真实实例 ShoppingCart realCart = (ShoppingCart) beanFactory.getBean(beanName); realCart.addItem(item); } }第四部分:高级篇——自定义作用域
在某些场景下,内置作用域无法满足需求。例如:
多租户SaaS应用:每个租户拥有独立的Bean实例。
线程作用域:同一个线程内共享Bean(非HTTP请求,如批量任务)。
事务作用域:在一个数据库事务内共享Bean。
下面我们将实现一个基于ThreadLocal的线程作用域。
Step 1: 实现Scope接口
java
public class ThreadScope implements Scope { private final ThreadLocal<Map<String, Object>> threadLocal = ThreadLocal.withInitial(HashMap::new); @Override public Object get(String name, ObjectFactory<?> objectFactory) { Map<String, Object> scopeMap = threadLocal.get(); Object scopedObject = scopeMap.get(name); if (scopedObject == null) { scopedObject = objectFactory.getObject(); scopeMap.put(name, scopedObject); } return scopedObject; } @Override public Object remove(String name) { return threadLocal.get().remove(name); } @Override public void registerDestructionCallback(String name, Runnable callback) { // 实际项目中可以在ThreadLocal中存储回调列表,在clear时执行 } @Override public Object resolveContextualObject(String key) { return null; // 支持EL表达式 } // 提供给外部清理的方法(如线程池任务结束时调用) public void clear() { threadLocal.remove(); } }Step 2: 注册自定义Scope
在Spring Boot中通过配置类注册:
java
@Configuration public class CustomScopeConfig { @Bean public static CustomScopeConfigurer threadScopeConfigurer() { CustomScopeConfigurer configurer = new CustomScopeConfigurer(); configurer.addScope("thread", new ThreadScope()); return configurer; } }Step 3: 使用自定义作用域
java
@Component @Scope("thread") public class TraceContext { private String traceId; // getters and setters } @Component public class TaskProcessor { @Autowired private TraceContext traceContext; public void process() { // 在同一个线程中,traceContext始终是同一个实例 traceContext.setTraceId(UUID.randomUUID().toString()); // 调用其他组件... } }Step 4: 生命周期管理与清理
为了防止内存泄漏,需要在任务执行完毕后清理ThreadLocal。可以借助ThreadPoolExecutor的钩子方法或AOP切面:
java
@Component public class ThreadScopeCleaner { @Autowired private ThreadScope threadScope; // 需要从CustomScopeConfigurer中获取或重新设计为单例 @Before("@annotation(com.example.TaskScoped)") public void clearBefore(JoinPoint point) { // 不清除,确保每次新任务都是干净的环境 threadScope.clear(); } }第五部分:性能与陷阱
1. 常见陷阱
| 陷阱 | 描述 | 解决方案 |
|---|---|---|
| 原型Bean注入单例导致失效 | 原型Bean在单例中只初始化一次,失去原型特性。 | 使用@Lookup或ObjectFactory延迟获取。 |
| Web作用域滥用 | 在非Web环境(如单元测试)中使用Request/Session作用域会报错。 | 测试时使用@WebAppConfiguration或MockRequestContextHolder。 |
| 序列化问题 | 作用域代理默认不支持序列化,如果Bean需要放入Session或分布式缓存可能报错。 | 配置proxyMode并实现Serializable,或使用Serializable代理模式。 |
| 内存泄漏(Session) | Session作用域Bean持有大量数据且Session未及时销毁。 | 确保Bean实现了HttpSessionBindingListener或使用@SessionAttributes配合清理。 |
2. 性能考量
Singleton:性能最好,无创建开销。
Prototype:创建开销大,频繁创建影响GC,适合轻量级对象。
Request/Session:涉及
ThreadLocal查找和代理调用,但开销极小(纳秒级),通常不是瓶颈。自定义Scope:需注意
ThreadLocal的清理,避免线程复用场景(如Tomcat线程池)下的内存泄漏。
第六部分:面试高频题解析
问:Spring中
@Scope注解的proxyMode到底解决了什么问题?
答:解决了生命周期较短的Bean注入到生命周期较长的Bean中时,生命周期不匹配的问题。通过代理,每次调用都去当前作用域中获取实例,保证了短生命周期Bean的正确隔离。问:Singleton Bean是线程安全的吗?
答:不是。Spring只是保证单例,不保证线程安全。如果Bean持有可变状态,需要开发者自行同步,或者将Bean设计为无状态。问:如何实现在Singleton Bean中每次调用都获取新的Prototype Bean?
答:① 使用@Lookup方法注入;② 注入ObjectFactory<T>,每次调用getObject();③ 实现ApplicationContextAware,每次手动getBean。
总结
| 作用域 | 数量 | 生命周期 | 典型应用 |
|---|---|---|---|
| Singleton | 1 | 容器启动~容器关闭 | Service, DAO, Config |
| Prototype | 每次获取新建 | 创建~GC | 有状态的模型 |
| Request | 每个请求1个 | HTTP请求周期 | Controller辅助类 |
| Session | 每个会话1个 | 用户会话周期 | 购物车、登录信息 |
| Application | Web应用1个 | ServletContext生命周期 | 全局Web配置 |
| Custom | 自定义 | 自定义 | 多租户、线程隔离 |
Spring的作用域机制是IoC容器灵活性的重要体现。掌握作用域不仅需要了解如何使用注解,更需要理解底层Scope接口的设计思想以及代理模式的运用。在架构设计时,合理选择作用域可以有效管理对象状态、提升系统性能并避免潜在的并发问题。