Bean 作用域详解 - singleton/prototype/request/session/application/websocket
概述
Spring IoC 容器管理的 Bean 在创建时,不仅可以决定其依赖关系和生命周期回调,还可以指定其作用域(Scope)。作用域决定了容器中 Bean 实例的存活范围、共享方式以及销毁时机。Spring 框架原生支持六种作用域,其中 singleton 和 prototype 可用于任何 Spring 应用,而 request、session、application 和 websocket 仅在 Web 容器中生效。
| 作用域 | 描述 | 适用容器 | 实例数量 | 线程安全 |
|---|---|---|---|---|
| singleton | 每个 Spring 容器只创建一个实例 | 任意 | 1 | 需自行保证 |
| prototype | 每次获取都创建新实例 | 任意 | 任意 | 每次新实例 |
| request | 每个 HTTP 请求一个实例 | Web 容器 | 每次请求 1 个 | 请求内安全 |
| session | 每个 HTTP Session 一个实例 | Web 容器 | 每个 Session 1 个 | Session 内安全 |
| application | 每个 ServletContext 一个实例 | Web 容器 | 1 | 需自行保证 |
| websocket | 每个 WebSocket Session 一个实例 | Web 容器 | 每个 WS Session 1 个 | Session 内安全 |
1. singleton 作用域
1.1 基本概念
singleton 是 Spring 的默认作用域。对于单例作用域的 Bean,IoC 容器在启动时(或首次请求时)创建唯一实例,并将其缓存到容器内部的单例缓存池中,后续所有对该 Bean 的请求都返回同一个实例。
@Component
public class DefaultScopeBean {
// 默认就是 singleton 作用域
}
@Component
@Scope("singleton")
public class ExplicitSingletonBean {
// 显式指定 singleton 作用域
}1.2 源码分析:AbstractBeanFactory.doGetBean() 中的缓存逻辑
Spring 单例 Bean 的缓存由 DefaultSingletonBeanRegistry 维护,AbstractBeanFactory 的 doGetBean() 方法是核心入口。以下为关键源码逻辑(基于 Spring Framework 6.x):
// AbstractBeanFactory.java
protected <T> T doGetBean(
String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly)
throws BeansException {
// 1. 从单例缓存中获取已创建的 Bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 缓存命中,返回实例(可能需要进行 FactoryBean 解包)
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// 2. 缓存未命中,检查是否在创建中(解决循环依赖)
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 3. 检查父容器
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// 委托给父容器
}
// 4. 记录正在创建的 Bean
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}
try {
// 5. 合并 BeanDefinition
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 6. 获取依赖的 Bean(先初始化依赖)
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
registerDependentBean(dep, beanName);
getBean(dep);
}
}
// 7. 根据作用域创建 Bean 实例
if (mbd.isSingleton()) {
// --- singleton 作用域:通过 lambda 回调延迟创建,并加入缓存 ---
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
// 创建失败则销毁已注册的单例
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
// --- prototype 作用域:每次都直接创建 ---
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
// --- 其他作用域(request/session 等):委托给 Scope 实现 ---
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
beanInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
});
beanInstance = getObjectForBeanInstance(beanInstance, name, beanName, mbd);
}
} catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
// 8. 类型适配
return adaptBeanInstance(name, beanInstance, requiredType);
}1.3 三级缓存机制(解决循环依赖)
DefaultSingletonBeanRegistry 维护了三级缓存来支持单例 Bean 的循环依赖:
// DefaultSingletonBeanRegistry.java
/** 一级缓存:已完成初始化的单例 Bean 池 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:早期暴露的单例 Bean(已完成实例化,尚未完成属性注入和初始化) */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
/** 三级缓存:单例工厂缓存(用于生成早期暴露对象的工厂) */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);三级缓存的工作流程如下:
- 实例化:通过反射创建 Bean 的原始实例,将其包装为
ObjectFactory放入三级缓存singletonFactories。 - 属性注入:填充 Bean 的属性。如果在此过程中遇到循环依赖的 Bean B,容器会尝试从缓存中获取:
- 一级缓存
singletonObjects:是否有已完成的 Bean B - 二级缓存
earlySingletonObjects:是否有早期的 Bean B - 三级缓存
singletonFactories:调用ObjectFactory.getEarlyBeanReference()获取早期引用,并提升到二级缓存
- 一级缓存
- 初始化:执行 BeanPostProcessor 的
postProcessBeforeInitialization、initMethod、postProcessAfterInitialization。 - 完成:将最终 Bean 放入一级缓存
singletonObjects,清理二、三级缓存。
// DefaultSingletonBeanRegistry.getSingleton() 方法
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 2. 从三级缓存获取 ObjectFactory,创建早期引用
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 3. 提升到二级缓存,删除三级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}2. prototype 作用域
2.1 基本概念
prototype 作用域意味着每次向容器请求该 Bean 时,都会创建一个全新的实例。Spring 容器不负责管理 prototype Bean 的完整生命周期——实例化、属性注入和初始化回调(InitializingBean、@PostConstruct)会被执行,但销毁回调(DisposableBean、@PreDestroy)不会被调用。
@Component
@Scope("prototype")
public class PrototypeTask {
private final UUID instanceId = UUID.randomUUID();
public void execute() {
System.out.println("Task instance: " + instanceId);
}
}2.2 源码分析:每次创建新实例的机制
在 AbstractBeanFactory.doGetBean() 中,prototype 的处理路径不经过 getSingleton() 缓存,而是直接调用 createBean():
// AbstractBeanFactory.doGetBean() - prototype 分支
else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
// 标记当前正在创建该 prototype Bean(用于检测循环依赖)
beforePrototypeCreation(beanName);
// 直接创建全新实例
prototypeInstance = createBean(beanName, mbd, args);
} finally {
// 清理创建标记
afterPrototypeCreation(beanName);
}
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}关键点:
- 无缓存:每次请求都执行
createBean(),因此prototype作用域不支持循环依赖——isPrototypeCurrentlyInCreation()检测到循环引用时会直接抛出异常。 - 生命周期管理:Spring 只负责"创建",不负责"销毁"。客户端代码需要自行管理 prototype Bean 的清理工作。
- 延迟加载:prototype Bean 总是懒加载的,只有在显式请求时才会被创建。
2.3 使用时注意事项
// 正确使用 prototype Bean 的方式
@Component
public class TaskManager {
@Autowired
private ApplicationContext applicationContext;
public void executeTask() {
// 必须通过容器获取才能得到新实例
PrototypeTask task = applicationContext.getBean(PrototypeTask.class);
task.execute();
}
}// ❌ 错误的注入方式 —— 仅在初始化时注入一次,失去 prototype 语义
@Component
public class WrongTaskManager {
@Autowired
private PrototypeTask task; // 注入的是单例!
public void executeTask() {
task.execute(); // 每次都是同一个实例
}
}3. request / session / application / websocket 作用域
3.1 启用 Web 作用域
这四种作用域仅在 Web 容器(如 Tomcat、Jetty)中生效。在 Spring Boot 应用中,引入 spring-boot-starter-web 后会自动注册相关 Scope。在传统 XML 配置中,需在 web.xml 中添加监听器:
<!-- web.xml -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>在 Spring Boot 中无需额外配置,自动生效。
3.2 request 作用域
每个 HTTP 请求会创建一个独立的 Bean 实例,请求处理完成后实例被销毁。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId = UUID.randomUUID().toString().substring(0, 8);
private String userId;
private String tenantId;
// getter / setter ...
}实现原理:RequestScope 内部使用 RequestAttributes 将 Bean 实例绑定到当前线程的 ServletRequest 属性中。每次请求通过 RequestContextHolder 获取当前请求的属性:
// RequestScope.java (Spring 内部实现简化)
public class RequestScope extends AbstractRequestScope {
@Override
protected Map<String, Object> getScopeMap(HttpServletRequest request) {
return new HashMap<String, Object>() {
@Override
public Object get(Object key) {
return request.getAttribute(key.toString());
}
@Override
public Object put(String key, Object value) {
request.setAttribute(key, value);
return null;
}
};
}
}3.3 session 作用域
每个 HTTP Session 对应一个 Bean 实例,在 Session 生命周期内共享,Session 失效时 Bean 被销毁。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
private String username;
private List<String> permissions;
private LocalDateTime loginTime;
// getter / setter ...
}典型应用场景:存储用户登录状态、购物车、权限信息等 Session 级数据。
3.4 application 作用域
整个 ServletContext 范围内只有一个 Bean 实例,类似于 singleton,但作用域范围是 ServletContext 级别(多个 Spring 容器共享同一个 ServletContext 时依然只有一个实例)。
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AppConfiguration {
private Map<String, Object> globalConfig = new ConcurrentHashMap<>();
// getter / setter ...
}3.5 websocket 作用域
每个 WebSocket Session 对应一个 Bean 实例,仅在 WebSocket 生命周期内有效。
@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketSessionContext {
private String sessionId;
private Map<String, Object> attributes = new HashMap<>();
// getter / setter ...
}3.6 作用域 Proxy 模式
Web 作用域 Bean 必须搭配 proxyMode 使用,原因在于这些 Bean 的生命周期与 Web 请求/会话绑定,而注入它们的 Bean 通常是 singleton(如 Controller、Service),在容器启动时就已经被创建。
Spring 通过创建 代理对象 来解决这个矛盾:注入的是一个代理(AOP 代理),实际方法调用时代理对象会根据当前请求/会话上下文获取真正的 Bean 实例。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// TARGET_CLASS:使用 CGLIB 代理
// INTERFACES:使用 JDK 动态代理(Bean 实现了接口时)
}@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES)
public class RequestScopedService implements MyService {
// 实现接口 ...
}4. @Scope 注解源码解析
4.1 注解定义
// org.springframework.context.annotation.Scope
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
/**
* 作用域名称,如 "singleton"、"prototype"、"request" 等
*/
@AliasFor("scopeName")
String value() default "singleton";
/**
* 作用域名称的别名
*/
@AliasFor("value")
String scopeName() default "singleton";
/**
* 代理模式,默认为 DEFAULT(不创建代理)
* 对于 request/session/application 等 Web 作用域,通常需要指定为
* ScopedProxyMode.TARGET_CLASS 或 ScopedProxyMode.INTERFACES
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}4.2 ScopedProxyMode 枚举
// org.springframework.context.annotation.ScopedProxyMode
public enum ScopedProxyMode {
/**
* 默认值,通常等于 NO
*/
DEFAULT,
/**
* 不创建代理对象
*/
NO,
/**
* 创建 JDK 动态代理,目标 Bean 必须实现至少一个接口
*/
INTERFACES,
/**
* 使用 CGLIB 创建类代理,不需要接口
*/
TARGET_CLASS
}4.3 @Scope 的处理流程
Spring 在解析 Bean 定义时,通过 AnnotationScopeMetadataResolver 解析 @Scope 注解:
// AnnotationScopeMetadataResolver.java
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
ScopeMetadata metadata = new ScopeMetadata();
if (definition instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
// 查找 @Scope 注解
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
annDef.getMetadata(), Scope.class);
if (attributes != null) {
// 设置作用域名称
metadata.setScopeName(attributes.getString("value"));
// 设置代理模式
ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
if (proxyMode == ScopedProxyMode.DEFAULT) {
proxyMode = ScopedProxyMode.NO;
}
metadata.setScopedProxyMode(proxyMode);
}
}
return metadata;
}解析后的 ScopeMetadata 由 ScopedProxyCreator 处理,如果 proxyMode 不为 NO,则创建作用域代理 BeanDefinition 替换原始 BeanDefinition:
// ScopedProxyCreator.java (简化)
public static void createScopedProxy(BeanDefinitionHolder definitionHolder,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
String originalBeanName = definitionHolder.getBeanName();
BeanDefinition targetDefinition = definitionHolder.getBeanDefinition();
// 1. 创建代理 BeanDefinition
RootBeanDefinition proxyDefinition = new RootBeanDefinition(
ScopedProxyFactoryBean.class);
proxyDefinition.getPropertyValues().add("targetBeanName", originalBeanName);
proxyDefinition.getPropertyValues().add("proxyTargetClass", proxyTargetClass);
// 2. 注册目标 Bean(使用 scopedTarget. 前缀)
BeanDefinitionHolder targetHolder = new BeanDefinitionHolder(
targetDefinition, originalBeanName + ".scopedTarget." + originalBeanName);
BeanDefinitionReaderUtils.registerBeanDefinition(targetHolder, registry);
// 3. 注册代理 Bean(使用原 Bean 名称)
BeanDefinitionReaderUtils.registerBeanDefinition(
new BeanDefinitionHolder(proxyDefinition, originalBeanName), registry);
}5. 混合作用域依赖问题
5.1 问题描述
当短生命周期的 Bean(如 request 作用域)被注入到长生命周期的 Bean(如 singleton 作用域的 Service)时,如果直接注入,短生命周期 Bean 仅会被创建一次,后续请求无法获取新实例:
@Component // singleton 作用域
public class OrderService {
@Autowired
private TenantContext tenantContext; // 期望是 request 作用域
public void processOrder() {
String tenantId = tenantContext.getTenantId(); // ❌ 永远是最初注入的值
}
}5.2 解决方案一:@Lazy 代理
@Component
public class OrderService {
@Lazy
@Autowired
private TenantContext tenantContext; // 注入的是懒加载代理
public void processOrder() {
// 每次访问时通过代理获取当前请求的 TenantContext
String tenantId = tenantContext.getTenantId();
}
}@Lazy 会为依赖生成一个懒加载代理,该代理只在首次被访问时才真正去容器中获取目标 Bean。结合 @Scope(proxyMode=...) 的自动代理,两者配合可以解决作用域冲突。
5.3 解决方案二:ScopedProxyMode
最推荐的方案——在定义 Bean 时直接指定 proxyMode:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {
// ...
}@Component
public class OrderService {
@Autowired
private TenantContext tenantContext; // 注入的是代理,安全
public void processOrder() {
String tenantId = tenantContext.getTenantId(); // ✅ 每次获取当前请求的值
}
}Spring 会为 TenantContext 创建一个 CGLIB 代理对象注入到 OrderService 中。每次调用代理的方法时,代理会从 RequestAttributes 中获取当前请求对应的真实实例来委托调用。
5.4 解决方案三:ObjectFactory / Provider
@Component
public class OrderService {
@Autowired
private ObjectFactory<TenantContext> tenantContextFactory;
public void processOrder() {
// 每次手动获取新实例
TenantContext context = tenantContextFactory.getObject();
String tenantId = context.getTenantId();
}
}或者使用 Jakarta 标准的 Provider:
@Component
public class OrderService {
@Autowired
private Provider<TenantContext> tenantContextProvider;
public void processOrder() {
TenantContext context = tenantContextProvider.get();
String tenantId = context.getTenantId();
}
}5.5 解决方案对比
| 方案 | 代码复杂度 | 性能开销 | 适用场景 |
|---|---|---|---|
| @Lazy + @Autowired | 低 | 低 | 简单场景,混合作用域依赖 |
| ScopedProxyMode | 低 | 中 | Web 作用域 Bean 的推荐方案 |
| ObjectFactory | 中 | 低 | 需要精细控制获取时机 |
| Provider | 中 | 低 | 符合 Jakarta 标准,解耦 |
6. 实战案例:SaaS 多租户系统中的 request 作用域
6.1 需求分析
在 SaaS 多租户系统中,每个 HTTP 请求都携带租户标识(如 Header 中的 X-Tenant-Id),系统需要根据当前租户切换数据源或隔离数据。使用 request 作用域可以优雅地管理租户上下文。
6.2 租户上下文定义
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {
private String tenantId;
private String userId;
private Locale locale;
private Map<String, Object> attributes = new HashMap<>();
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Locale getLocale() {
return locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T getAttribute(String key) {
return (T) attributes.get(key);
}
}6.3 拦截器:解析请求中的租户信息
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Autowired
private TenantContext tenantContext;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 1. 从请求头中获取租户 ID
String tenantId = request.getHeader("X-Tenant-Id");
if (tenantId == null || tenantId.isBlank()) {
// 允许默认租户(或返回 400)
tenantId = "default";
}
// 2. 填充租户上下文
tenantContext.setTenantId(tenantId);
tenantContext.setUserId(request.getHeader("X-User-Id"));
tenantContext.setLocale(request.getLocale());
// 3. 设置租户 ID 到日志 MDC(便于日志追踪)
org.slf4j.MDC.put("tenantId", tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后清理 MDC
org.slf4j.MDC.clear();
}
}6.4 注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final TenantInterceptor tenantInterceptor;
public WebConfig(TenantInterceptor tenantInterceptor) {
this.tenantInterceptor = tenantInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor)
.addPathPatterns("/api/**");
}
}6.5 在 Service 层使用租户上下文
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private TenantContext tenantContext; // 注入的是作用域代理
@Autowired
private OrderRepository orderRepository;
public List<Order> getCurrentTenantOrders() {
// 1. 获取当前租户 ID
String tenantId = tenantContext.getTenantId();
// 2. 根据租户查询数据(多租户隔离)
return orderRepository.findByTenantId(tenantId);
}
public void createOrder(Order order) {
// 自动填充租户信息
order.setTenantId(tenantContext.getTenantId());
order.setCreatedBy(tenantContext.getUserId());
orderRepository.save(order);
}
}6.6 测试验证
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@SpringBootTest
@WebAppConfiguration
public class TenantContextTest {
@Autowired
private ApplicationContext applicationContext;
@Test
void testTenantContextIsolation() {
// 模拟请求 1
MockHttpServletRequest request1 = new MockHttpServletRequest();
request1.addHeader("X-Tenant-Id", "tenant-a");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request1));
TenantContext context1 = applicationContext.getBean(TenantContext.class);
assertThat(context1.getTenantId()).isEqualTo("tenant-a");
// 切换请求
MockHttpServletRequest request2 = new MockHttpServletRequest();
request2.addHeader("X-Tenant-Id", "tenant-b");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request2));
TenantContext context2 = applicationContext.getBean(TenantContext.class);
assertThat(context2.getTenantId()).isEqualTo("tenant-b");
// 验证两个请求的上下文相互隔离
assertThat(context1).isNotSameAs(context2);
// 清理
RequestContextHolder.resetRequestAttributes();
}
}7. 自定义作用域
如果内置的六种作用域无法满足需求,Spring 允许通过实现 Scope 接口来自定义作用域:
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
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);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// ThreadScope 不管理销毁回调
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return String.valueOf(Thread.currentThread().getId());
}
}注册自定义作用域:
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer scopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new ThreadScope());
return configurer;
}
}总结
| 特性 | singleton | prototype | request | session | application | websocket |
|---|---|---|---|---|---|---|
| 默认值 | ✅ 是 | ❌ | ❌ | ❌ | ❌ | ❌ |
| 容器管理销毁 | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| 支持懒加载 | ✅(默认非懒加载) | ✅(默认懒加载) | ✅ | ✅ | ✅ | ✅ |
| 需要代理 | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| 循环依赖支持 | ✅(三级缓存) | ❌ | 仅代理间 | 仅代理间 | 仅代理间 | 仅代理间 |
| 线程安全要求 | 高 | 低 | 低 | 中 | 高 | 中 |
选择作用域的核心原则:
- 无状态 Bean(如 Service、Repository)→
singleton - 有状态且非线程安全 →
prototype或request - 与用户会话绑定的数据 →
session - 仅请求内有效的临时数据 →
request - 全局跨请求的配置数据 →
application - WebSocket 连接内的状态 →
websocket