引言
Spring 框架作为 Java 企业级开发中最主流的轻量级容器框架,自 2003 年发布以来,凭借其强大的依赖注入(DI)、面向切面编程(AOP)、事务管理、MVC 架构等特性,极大地简化了企业应用的开发流程。而在 Spring 的众多核心概念中,Bean 的作用域(Scope) 是一个看似简单却极其关键的机制。
初学者在使用 Spring 时,往往只关注如何通过 @Component、@Service 或 XML 配置来定义 Bean,而忽略了 Bean 的生命周期和作用范围。这可能导致诸如“单例 Bean 中保存了用户状态”、“多线程环境下数据污染”、“内存泄漏”等严重问题。因此,深入理解 Spring 的 Scope 属性,不仅是掌握 Spring 容器行为的基础,更是编写高性能、高可靠系统的关键。
本文将从零开始,系统性地讲解 Spring 中的 Scope 概念,涵盖以下内容:
- 什么是 Scope?为什么需要它?
- Spring 内置的五种标准 Scope(singleton、prototype、request、session、application)详解
- Web 环境下的 Scope 实现原理(ScopedProxy、RequestContextHolder 等)
- 自定义 Scope 的实现方法
- 常见误区与最佳实践
- 实战案例分析
- Spring Boot 中的 Scope 使用差异
无论你是刚接触 Spring 的新手,还是已有一定经验但对 Scope 理解不深的开发者,本文都将为你提供全面、深入且实用的知识体系。
第一章:Scope 的基本概念
1.1 什么是 Scope?
在 Spring 中,Scope(作用域) 定义了 Bean 在容器中的生命周期和可见范围。换句话说,Scope 决定了:
- 容器何时创建该 Bean?
- 创建多少个实例?
- 这些实例在什么上下文中共享?
- 何时销毁?
例如,一个 singleton Scope 的 Bean 在整个应用生命周期中只会被创建一次,并由所有请求共享;而 prototype Scope 的 Bean 每次请求都会创建一个新实例。
1.2 为什么需要 Scope?
设想一个 Web 应用场景:
- 用户 A 登录后,系统需要记录其用户 ID。
- 用户 B 同时登录。
如果我们将用户信息存储在一个 @Service 类的成员变量中,而该 Service 默认是 singleton(单例),那么用户 A 和 B 的数据会互相覆盖!这就是典型的“状态污染”问题。
通过合理使用 Scope(如将用户上下文 Bean 设为 request 或 session),我们可以确保每个用户的操作互不干扰。
此外,Scope 还影响内存使用、性能和线程安全。例如:
- 单例 Bean 节省内存,但必须是无状态的;
- 原型 Bean 每次新建,开销大但隔离性好;
- 请求作用域适合临时数据,自动随请求结束而清理。
因此,选择合适的 Scope 是设计健壮 Spring 应用的前提。
第二章:Spring 内置的标准 Scope
Spring 提供了五种标准 Scope,其中前两种适用于所有环境,后三种仅在 Web 环境中可用。
2.1 singleton(单例)
2.1.1 定义
- 默认 Scope。
- 在 Spring IoC 容器中,每个 Bean 定义只对应一个实例。
- 该实例在容器启动时(或首次被请求时,若配置为懒加载)创建,并在整个容器生命周期内复用。
2.1.2 配置方式
// 注解方式(默认即为 singleton)
@Component
@Scope("singleton") // 可省略
public class UserService {
// ...
}
// XML 方式
<bean id="userService" class="com.example.UserService" scope="singleton"/>
2.1.3 特点
- 线程安全问题:由于所有线程共享同一个实例,不能在单例 Bean 中保存可变状态(如用户 ID、临时缓存等)。
- 性能优势:避免频繁创建/销毁对象,减少 GC 压力。
- 初始化时机:默认在容器启动时初始化(可通过
@Lazy延迟)。
2.1.4 示例
@Service
public class CounterService {
private int count = 0; // ❌ 危险!非线程安全
public void increment() {
count++; // 多线程下结果不可预测
}
public int getCount() {
return count;
}
}
正确做法:使用
ThreadLocal、方法参数传递状态,或将该 Bean 改为prototype。
2.2 prototype(原型)
2.2.1 定义
- 每次从容器获取该 Bean 时,都会创建一个新实例。
- 容器不负责管理其生命周期(不会调用销毁回调)。
2.2.2 配置方式
@Component
@Scope("prototype")
public class OrderProcessor {
// 每次注入或 getBean 都是新实例
}
2.2.3 特点
- 无状态隔离:每个调用者拥有独立实例,天然线程安全。
- 内存开销大:频繁创建对象可能影响性能。
- 生命周期不受控:Spring 不会调用
@PreDestroy方法(除非手动管理)。
2.2.4 注意事项
@Service
public class OrderService {
@Autowired
private OrderProcessor processor; // ❌ 问题:processor 是单例 OrderService 的成员,只会注入一次!
public void processOrder(Order order) {
processor.handle(order); // 始终是同一个 processor 实例!
}
}
解决方案:使用
ObjectProvider或ApplicationContext动态获取:
@Service
public class OrderService {
@Autowired
private ObjectProvider<OrderProcessor> processorProvider;
public void processOrder(Order order) {
OrderProcessor processor = processorProvider.getIfAvailable();
processor.handle(order);
}
}
2.3 request(请求作用域)
2.3.1 定义
- 仅在 Web 应用中有效。
- 每个 HTTP 请求创建一个新实例,请求结束时销毁。
2.3.2 配置方式
@Component
@Scope("request")
public class RequestContext {
private String userId;
// getter/setter
}
2.3.3 底层原理
Spring 通过 RequestContextHolder 绑定当前请求的 HttpServletRequest,并在需要时从 ServletRequestAttributes 中获取或创建 Bean。
2.3.4 使用场景
- 存储当前请求的用户信息、请求 ID、TraceID 等。
- 避免在 Controller 中层层传递参数。
2.3.5 示例
@RestController
public class UserController {
@Autowired
private RequestContext requestContext;
@GetMapping("/user")
public String getUser(@RequestParam String userId) {
requestContext.setUserId(userId);
return userService.getCurrentUser(); // 内部可直接使用 requestContext
}
}
注意:
requestBean 不能被singletonBean 直接注入(会报错),需使用代理(见第四章)。
2.4 session(会话作用域)
2.4.1 定义
- 每个 HTTP Session 对应一个 Bean 实例。
- Session 销毁时,Bean 也被销毁。
2.4.2 配置
@Component
@Scope("session")
public class UserSession {
private String username;
// ...
}
2.4.3 使用场景
- 用户登录状态管理。
- 购物车(短期会话存储)。
2.4.4 注意事项
- 长时间不活跃的 Session 会被服务器回收,Bean 随之销毁。
- 不适合存储大量数据(占用内存)。
2.5 application(应用作用域)
2.5.1 定义
- 作用范围为
ServletContext,整个 Web 应用共享一个实例。 - 类似于
singleton,但绑定到 Web 应用上下文。
2.5.2 配置
@Component
@Scope("application")
public class AppConfig {
private String appName = "MyApp";
}
2.5.3 与 singleton 的区别
| 对比项 | singleton | application |
|---|---|---|
| 容器范围 | Spring ApplicationContext | ServletContext |
| 可见性 | 所有 Spring Bean | Web 应用内所有组件(包括非 Spring) |
| 使用场景 | 通用服务类 | Web 全局配置 |
实际开发中,
application很少使用,通常用singleton+@Value读取配置即可。
第三章:Web 环境下的 Scope 实现机制
3.1 为什么 singleton 不能直接注入 request Bean?
考虑以下代码:
@Service
public class UserService {
@Autowired
private RequestContext requestContext; // request scope
}
在容器启动时,UserService(singleton)被创建,此时没有 HTTP 请求上下文,Spring 无法确定应该创建哪个 RequestContext 实例,因此会抛出异常:
Error creating bean with name 'userService':
Scope 'request' is not active for the current thread...
3.2 解决方案:Scoped Proxy(作用域代理)
Spring 提供了代理机制来解决此问题。
3.2.1 使用 proxyMode
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
// ...
}
proxyMode = TARGET_CLASS:使用 CGLIB 生成子类代理。proxyMode = INTERFACES:基于接口的 JDK 动态代理(要求 Bean 实现接口)。
3.2.2 代理工作原理
- 容器为
RequestContext创建一个代理对象(如RequestContext$$EnhancerBySpringCGLIB)。 - 将该代理注入到
UserService中。 - 当调用
requestContext.getUserId()时,代理会:- 检查当前线程是否有活跃的 HTTP 请求;
- 若有,则从
RequestContextHolder获取真实的RequestContext实例; - 调用真实对象的方法。
这样,singleton Bean 持有的是一个“延迟解析”的代理,实际行为在运行时根据上下文动态决定。
3.2.3 示例验证
@Service
public class UserService {
@Autowired
private RequestContext requestContext; // 实际是代理对象
public String getCurrentUser() {
// 此时才真正从当前请求中获取 RequestContext 实例
return requestContext.getUserId();
}
}
✅ 安全!线程隔离,无状态污染。
3.3 RequestContextHolder 与 ThreadLocal
Spring 通过 RequestContextHolder 将请求上下文绑定到当前线程:
// 内部使用 ThreadLocal 存储
private static final ThreadLocal<RequestAttributes> requestContextHolder =
new NamedThreadLocal<>("Request context");
- 每个 HTTP 请求由一个线程处理(传统 Servlet 模型)。
DispatcherServlet在请求开始时调用RequestContextHolder.setRequestAttributes()。- 请求结束时清除。
注意:在异步线程(如
@Async、线程池)中,RequestContextHolder默认为空。需手动传递:
@Async
public void asyncTask() {
RequestAttributes attrs = RequestContextHolder.currentRequestAttributes();
// 在新线程中设置
RequestContextHolder.setRequestAttributes(attrs, true);
// ... 业务逻辑
}
第四章:自定义 Scope
Spring 允许开发者定义自己的 Scope,以满足特殊需求(如 WebSocket 作用域、租户作用域等)。
4.1 实现步骤
- 实现
Scope接口; - 注册到容器;
- 使用
@Scope注解指定。
4.2 示例:实现一个 "thread" Scope
目标:每个线程拥有独立的 Bean 实例。
4.2.1 实现 Scope 接口
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadLocal =
new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadLocal.get();
Object obj = scope.get(name);
if (obj == null) {
obj = objectFactory.getObject();
scope.put(name, obj);
}
return obj;
}
@Override
public Object remove(String name) {
return threadLocal.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 可选:注册销毁回调
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
4.2.2 注册 Scope
@Configuration
public class CustomScopeConfig implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
beanFactory.registerScope("thread", new ThreadScope());
}
}
4.2.3 使用
@Component
@Scope("thread")
public class ThreadLocalData {
private String data;
// getter/setter
}
现在,每个线程调用 getBean("threadLocalData") 都会获得独立实例。
第五章:常见误区与最佳实践
5.1 误区一:在 singleton 中保存用户状态
错误示例:
@Service
public class AuthService {
private String currentUserId; // ❌ 共享状态!
public void login(String userId) {
this.currentUserId = userId;
}
public String getCurrentUser() {
return currentUserId;
}
}
后果:多用户并发时,currentUserId 被覆盖。
正确做法:
- 使用
requestScope; - 通过方法参数传递;
- 使用
SecurityContext(Spring Security)。
5.2 误区二:prototype Bean 被 singleton 持有
如前所述,直接注入会导致 prototype 失效。
解决方案:
- 使用
ObjectProvider<T>; - 注入
ApplicationContext,手动调用getBean(); - 使用
@Lookup方法(XML 风格,已过时)。
5.3 误区三:忽略 Web Scope 的代理需求
未设置 proxyMode 导致启动失败或运行时异常。
建议:只要 Web Scope Bean 被其他 Scope 注入,一律加上代理。
5.4 最佳实践总结
| 场景 | 推荐 Scope | 说明 |
|---|---|---|
| 无状态服务类(Service、DAO) | singleton | 默认,高效 |
| 有状态、每次需新实例 | prototype | 配合 ObjectProvider 使用 |
| 请求级数据(TraceID、用户ID) | request + 代理 | 确保线程隔离 |
| 会话级数据(登录态) | session + 代理 | 注意内存 |
| 全局配置 | singleton 或 application | 优先 singleton |
| 特殊上下文(租户、设备) | 自定义 Scope | 灵活扩展 |
第六章:实战案例
案例:实现多租户 SaaS 系统的 TenantContext
需求:每个请求携带 X-Tenant-ID 头,系统需自动识别租户并隔离数据。
步骤 1:定义 TenantContext(request scope)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {
private String tenantId;
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getTenantId() {
return tenantId;
}
}
步骤 2:编写拦截器提取租户 ID
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Autowired
private TenantContext tenantContext;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
if (tenantId == null) {
throw new IllegalArgumentException("Missing X-Tenant-ID");
}
tenantContext.setTenantId(tenantId);
return true;
}
}
步骤 3:在 Service 中使用
@Service
public class OrderService {
@Autowired
private TenantContext tenantContext;
public List<Order> getOrders() {
String tenantId = tenantContext.getTenantId(); // 自动获取当前租户
return orderRepository.findByTenant(tenantId);
}
}
✅ 实现了租户数据自动隔离,且无需在每个方法中传递 tenantId。
第七章:Spring Boot 中的 Scope 差异
Spring Boot 默认启用 Web 环境(spring-boot-starter-web),因此 request、session 等 Scope 可直接使用。
7.1 自动配置支持
DispatcherServlet自动注册;RequestContextListener或RequestContextFilter自动配置,确保RequestContextHolder可用。
7.2 测试中的 Scope 处理
在单元测试中,Web Scope 无法直接使用。需使用 @WebMvcTest 或手动模拟:
@Test
@ContextConfiguration
public class RequestContextTest {
@Test
public void testRequestScope() {
try (MockHttpServletRequest request = new MockHttpServletRequest()) {
RequestContextHolder.setRequestAttributes(
new ServletRequestAttributes(request)
);
// 此时可安全使用 request scope bean
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
结语
Spring 的 Scope 机制虽小,却是构建可靠、可维护系统的重要基石。理解不同 Scope 的生命周期、适用场景及底层原理,能帮助我们避免常见的并发与状态管理陷阱。
记住:
- 无状态用 singleton,有状态看上下文;
- Web Scope 必须代理;
- prototype 不等于“每次 new”,需动态获取;
- 自定义 Scope 是高级扩展手段。
希望本文能为你打开 Spring Scope 的深度认知之门。在实际项目中,合理运用这些知识,你将写出更优雅、更健壮的代码。
参考资料
- Spring Framework 官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes
- 《Spring 实战(第6版)》
- Spring 源码:
org.springframework.beans.factory.config.Scope- Spring Boot 自动配置源码
1434

被折叠的 条评论
为什么被折叠?



