前言什么是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中有可变状态如实例变量在多线程环境下会出现并发问题。适用场景无状态的服务层BeanService、DAO层、配置类、工具类。配置方式java// 注解方式 Component Scope(singleton) // 或者不写默认就是singleton public class UserService {} // XML方式 bean iduserService classcom.example.UserService scopesingleton/2. Prototype原型定义每次对该Bean的请求如调用getBean或依赖注入容器都会创建一个全新的实例。特点生命周期容器只负责创建和初始化不负责销毁。创建后容器不再持有该实例的引用销毁方法如PreDestroy通常不会被调用。性能开销每次获取都涉及对象的创建和依赖注入高频创建时需考虑GC压力。状态管理适合持有私有状态的对象每个调用者拥有独立副本。适用场景有状态的对象如每次请求携带不同用户数据的Model、多线程环境下的非线程安全对象。重要陷阱如果在Singleton Bean中注入Prototype Bean由于Singleton只实例化一次注入过程也只发生一次因此Prototype Bean会退化为单例行为。解决方案方法注入Method Injection使用Lookup注解。作用域代理Scoped Proxy结合proxyMode。代码示例javaComponent Scope(value prototype) public class ShoppingCart { private ListItem 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. WebSocketWebSocket会话定义每个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 MapString, 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配置xmlbean idshoppingCart classcom.example.ShoppingCart scopesession aop:scoped-proxy proxy-target-classtrue/ /bean方式二注解配置javaComponent 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接口javapublic class ThreadScope implements Scope { private final ThreadLocalMapString, Object threadLocal ThreadLocal.withInitial(HashMap::new); Override public Object get(String name, ObjectFactory? objectFactory) { MapString, 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中通过配置类注册javaConfiguration public class CustomScopeConfig { Bean public static CustomScopeConfigurer threadScopeConfigurer() { CustomScopeConfigurer configurer new CustomScopeConfigurer(); configurer.addScope(thread, new ThreadScope()); return configurer; } }Step 3: 使用自定义作用域javaComponent 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切面javaComponent 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代理模式。内存泄漏SessionSession作用域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方法注入② 注入ObjectFactoryT每次调用getObject()③ 实现ApplicationContextAware每次手动getBean。总结作用域数量生命周期典型应用Singleton1容器启动~容器关闭Service, DAO, ConfigPrototype每次获取新建创建~GC有状态的模型Request每个请求1个HTTP请求周期Controller辅助类Session每个会话1个用户会话周期购物车、登录信息ApplicationWeb应用1个ServletContext生命周期全局Web配置Custom自定义自定义多租户、线程隔离Spring的作用域机制是IoC容器灵活性的重要体现。掌握作用域不仅需要了解如何使用注解更需要理解底层Scope接口的设计思想以及代理模式的运用。在架构设计时合理选择作用域可以有效管理对象状态、提升系统性能并避免潜在的并发问题。