Spring Bean作用域全解析
🌟 一、核心思想:什么是 Bean Scope?
Bean Definition 是“配方”,Bean Scope 决定这个配方能做出多少份“成品”以及它们的生命周期。
- 在 Spring 中,一个
<bean>或@Component注解定义的类,并不直接等于一个 Java 对象。 - 它是一个“创建对象的配方”(a recipe for creating instances)。
- 而 Scope(作用域)决定了这个配方会被执行多少次、每次创建的对象是否共享、何时创建、何时销毁。
例如:
<bean id="userService" class="com.example.UserServiceImpl" scope="singleton"/>
这句配置的意思是:“用 UserServiceImpl 这个类作为模板,在 Spring 容器中只创建 一个实例 并全局共享”。
📚 二、Spring 支持的 6 种标准 Bean Scope
| Scope | 中文含义 | 是否 Web 环境专用 | 描述 |
|---|---|---|---|
singleton | 单例 | ❌ 否 | 默认值,每个 Spring 容器中只创建一个实例,所有请求共用同一个对象。 |
prototype | 原型 | ❌ 否 | 每次获取 bean 都会创建一个新实例。 |
request | 请求级 | ✅ 是 | 每个 HTTP 请求创建一个实例,请求结束即销毁。 |
session | 会话级 | ✅ 是 | 每个用户会话(Session)创建一个实例,会话结束销毁。 |
application | 应用级 | ✅ 是 | 整个 Web 应用(ServletContext)生命周期内只有一个实例。 |
websocket | WebSocket 级 | ✅ 是 | 每个 WebSocket 连接对应一个 bean 实例。 |
💡 提示:
threadscope 存在但默认未注册,需手动启用。
🧱 三、详解每种 Scope 的行为与使用场景
1. singleton(单例)—— 默认且最常用
✅ 特点:
- 整个 Spring IoC 容器中 只有一个实例。
- 实例被创建后会被缓存,后续所有依赖注入或
getBean()调用都返回同一个对象。 - 生命周期由 Spring 容器管理(初始化、销毁回调都会执行)。
⚠️ 注意:
- Spring 的
singleton≠ GoF 设计模式中的单例。- GoF 单例:JVM 类加载器级别,保证整个 JVM 中只有一个。
- Spring 单例:容器级别,多个 Spring 容器可以有多个“单例”实例。
✅ 示例:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 或显式声明 -->
<bean id="accountService" class="..." scope="singleton"/>
📌 适用场景:无状态的服务类(如 Service、DAO),大多数 Bean 都应使用 singleton。
2. prototype(原型)—— 每次都要新的
✅ 特点:
- 每次通过
getBean()或依赖注入时,都会 新建一个实例。 - Spring 容器只负责初始化和装配,不管理其后续生命周期。
- 不会调用 destroy 方法(@PreDestroy / destroy-method)!
⚠️ 重点警告:
“Spring 不会追踪 prototype bean”,所以:
- 如果 prototype bean 持有数据库连接、文件句柄等资源,必须由客户端代码手动释放。
- 否则会造成内存泄漏。
✅ 示例:
<bean id="command" class="com.example.Command" scope="prototype"/>
📌 适用场景:有状态的对象(如用户表单命令对象)、每次需要独立状态的组件。
3. request / session / application / websocket(Web 专属作用域)
这些作用域只能在 Web-aware 的 ApplicationContext(如 XmlWebApplicationContext)中使用。否则会抛出异常。
🔧 前置配置要求:
为了让 Spring 能感知当前请求/会话,必须注册 RequestContextListener 或 RequestContextFilter。
方式一:Listener(推荐)
<listener><listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
方式二:Filter(兼容旧容器)
<filter><filter-name>requestContextFilter</filter-name><filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping><filter-name>requestContextFilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>
✅ DispatcherServlet 已经自动完成绑定,MVC 场景下无需额外配置。
🌐 request 作用域
- 每个 HTTP 请求创建一个实例。
- 请求处理完后实例被销毁。
<bean id="loginAction" class="com.example.LoginAction" scope="request"/>
📌 适用场景:处理请求的控制器辅助类、请求上下文对象。
🧑💼 session 作用域
- 每个用户会话(Session)持有一个实例。
- 用户退出或 Session 过期时销毁。
<bean id="userPreferences" class="com.example.UserPreferences" scope="session"/>
📌 适用场景:用户偏好设置、购物车、登录状态等。
🏢 application 作用域
- 整个 Web 应用(ServletContext)生命周期内仅一个实例。
- 类似于 singleton,但它是绑定到
ServletContext属性上的。
<bean id="appConfig" class="com.example.AppConfig" scope="application"/>
📌 适用场景:全局配置、应用级缓存。
🔗 websocket 作用域
- 每个 WebSocket 连接对应一个 bean 实例。
- WebSocket 关闭时销毁。
@Scope("websocket")
@Component
public class ChatHandler { ... }
📌 适用场景:聊天室、实时通知等长连接服务。
🔄 四、关键问题:Singleton Bean 注入 Prototype Bean 怎么办?
这是一个经典陷阱!
❌ 问题描述:
<bean id="prototypeBean" class="com.example.MyPrototype" scope="prototype"/>
<bean id="singletonBean" class="com.example.MySingleton"><property name="myPrototype" ref="prototypeBean"/>
</bean>
你以为每次调用 singletonBean 都会拿到一个新的 prototypeBean?
错! Spring 在初始化 singletonBean 时就完成了依赖注入,只会注入 一次 prototypeBean 实例。
也就是说:prototype 失效了!
✅ 解决方案:方法注入(Method Injection)
Spring 提供了两种优雅方式来解决这个问题:
方案 1:使用 lookup method(XML 配置)
<bean id="myCommand" class="com.example.MyCommand" scope="prototype"/><bean id="commandManager" class="com.example.CommandManager"><lookup-method name="createCommand" bean="myCommand"/>
</bean>
Java 类中定义抽象方法:
public abstract class CommandManager {public Object process() {MyCommand command = createCommand(); // 每次调用都返回新实例return command.execute();}protected abstract MyCommand createCommand();
}
Spring 会在运行时用 CGLIB 动态生成子类,覆盖 createCommand() 方法,使其每次都返回新的 prototype 实例。
方案 2:使用 ObjectFactory 或 Provider(推荐,现代写法)
@Component
public class CommandManager {@Autowiredprivate ObjectFactory<MyCommand> commandFactory;public void process() {MyCommand cmd = commandFactory.getObject(); // 每次获取新实例cmd.execute();}
}
或者使用 JSR-330 的 Provider:
@Inject
private Provider<MyCommand> commandProvider;public void process() {MyCommand cmd = commandProvider.get(); // 每次都是新实例
}
✅ 推荐使用
Provider<T>,类型安全、易于测试。
🔐 五、Scoped Beans 作为依赖时的代理机制(aop:scoped-proxy/)
❓ 问题:为什么要把 session bean 注入 singleton bean 时要用代理?
设想:
<bean id="cart" class="Cart" scope="session"/>
<bean id="orderService" class="OrderService"><property name="cart" ref="cart"/>
</bean>
如果不加代理:
orderService是 singleton,启动时注入cart。- 但此时没有 HTTP Session,无法确定是哪个用户的
cart。 - 即使有,也只能注入一个用户的
cart,所有用户共用,出错!
✅ 正确做法:使用 <aop:scoped-proxy/>
<bean id="cart" class="Cart" scope="session"><aop:scoped-proxy/>
</bean><bean id="orderService" class="OrderService"><property name="cart" ref="cart"/>
</bean>
🧩 原理:
- Spring 创建一个 代理对象(Proxy),它实现了
Cart接口。 - 当
orderService调用cart.addItem(...)时,代理会:- 查找当前线程绑定的 HTTP Session;
- 从中取出该用户的
Cart实例; - 将方法调用委托给真实的
Cart对象。
✅ 这样每个用户看到的都是自己的购物车,而
orderService是共享的。
🛠️ 选择代理类型
| 方式 | 配置 | 说明 |
|---|---|---|
| CGLIB 类代理(默认) | <aop:scoped-proxy/> | 不需要接口,但只能代理 public 方法 |
| JDK 接口代理 | <aop:scoped-proxy proxy-target-class="false"/> | 必须实现接口,适合面向接口编程 |
✅ 建议:如果 bean 实现了接口,优先使用 JDK 代理。
🧩 六、自定义作用域(Custom Scope)
Spring 允许扩展作用域机制。
步骤 1:实现 org.springframework.beans.factory.config.Scope 接口
public class ThreadScope implements Scope {@Overridepublic Object get(String name, ObjectFactory<?> objectFactory) {// 从当前线程获取或创建对象}@Overridepublic Object remove(String name) {// 从当前线程移除对象}@Overridepublic void registerDestructionCallback(String name, Runnable callback) {// 注册线程结束时的清理逻辑}@Overridepublic String getConversationId() {return Thread.currentThread().getName();}
}
步骤 2:注册自定义作用域
方式一:编程式注册
ConfigurableBeanFactory beanFactory = ...;
beanFactory.registerScope("thread", new ThreadScope());
方式二:声明式注册(XML)
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"><property name="scopes"><map><entry key="thread"><bean class="org.springframework.context.support.SimpleThreadScope"/></entry></map></property>
</bean><bean id="thing" class="x.y.Thing" scope="thread"><aop:scoped-proxy/>
</bean>
✅
SimpleThreadScope是 Spring 自带但未注册的线程级作用域,可用于线程内共享对象。
✅ 七、总结:一张表掌握所有 Scope
| Scope | 创建时机 | 销毁时机 | 是否共享 | 使用条件 | 典型用途 |
|---|---|---|---|---|---|
| singleton | 容器启动 | 容器关闭 | 是 | 所有环境 | 服务类、DAO |
| prototype | 每次请求 | 不管理 | 否 | 所有环境 | 有状态对象 |
| request | 每个请求 | 请求结束 | 否 | Web | 请求处理器 |
| session | 新 Session | Session 失效 | 否 | Web | 用户偏好、购物车 |
| application | 应用启动 | 应用停止 | 是 | Web | 全局配置 |
| websocket | 新连接 | 连接关闭 | 否 | Web | 实时通信 |
🎯 八、最佳实践建议
- 默认使用
singleton:大多数 Bean 都应该是无状态的,适合单例。 - 慎用
prototype:确保不会造成资源泄漏,必要时手动清理。 - Web 作用域 + 代理:注入短生命周期 bean 到长生命周期 bean 时,必须使用
<aop:scoped-proxy/>。 - 优先使用
Provider<T>替代 lookup method,更现代、更灵活。 - 避免覆盖内置 scope:不要尝试重写
singleton或prototype。 - 注意线程安全:singleton bean 如果有成员变量,要注意并发访问问题。
📚 九、附:现代注解写法对照
| XML 配置 | Java 注解写法 |
|---|---|
<bean scope="request"> | @RequestScope + @Component |
<bean scope="session"> | @SessionScope + @Component |
<bean scope="application"> | @ApplicationScope + @Component |
<aop:scoped-proxy/> | @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) |
示例:
@SessionScope
@Component
public class UserSessionData {// ...
}
或指定代理模式:
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class Cart { ... }
✅ 总结一句话:
Bean Scope 是 Spring 控制对象生命周期和可见范围的强大机制,通过配置而非硬编码来决定对象的创建方式,极大提升了灵活性和可维护性。
理解了作用域,你就掌握了 Spring 容器如何“按需造物”的秘密。
如需进一步深入,可研究:
- AOP 代理底层原理(CGLIB vs JDK Dynamic Proxy)
ObjectFactory与Provider的源码实现- 自定义 Scope 在分布式上下文传递中的应用(如 TraceId 透传)
希望这份详解能帮你彻底吃透 Spring Bean Scopes!如果你有具体使用场景的问题,也欢迎继续提问。
