Spring Bean作用域全解析

🌟 一、核心思想:什么是 Bean Scope?

Bean Definition 是"配方",Bean Scope 决定这个配方能做出多少份"成品"以及它们的生命周期。

  • 在 Spring 中,一个 <bean>@Component 注解定义的类,并不直接等于一个 Java 对象。
  • 它是一个"创建对象的配方"(a recipe for creating instances)。
  • Scope(作用域)决定了这个配方会被执行多少次、每次创建的对象是否共享、何时创建、何时销毁

例如:

xml 复制代码
<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 实例。

💡 提示:thread scope 存在但默认未注册,需手动启用。


🧱 三、详解每种 Scope 的行为与使用场景

1. singleton(单例)------ 默认且最常用

✅ 特点:
  • 整个 Spring IoC 容器中 只有一个实例
  • 实例被创建后会被缓存,后续所有依赖注入或 getBean() 调用都返回同一个对象。
  • 生命周期由 Spring 容器管理(初始化、销毁回调都会执行)。
⚠️ 注意:
  • Spring 的 singleton ≠ GoF 设计模式中的单例。
    • GoF 单例:JVM 类加载器级别,保证整个 JVM 中只有一个。
    • Spring 单例:容器级别,多个 Spring 容器可以有多个"单例"实例。
✅ 示例:
xml 复制代码
<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 持有数据库连接、文件句柄等资源,必须由客户端代码手动释放
  • 否则会造成内存泄漏。
✅ 示例:
xml 复制代码
<bean id="command" class="com.example.Command" scope="prototype"/>

📌 适用场景:有状态的对象(如用户表单命令对象)、每次需要独立状态的组件。


3. request / session / application / websocket(Web 专属作用域)

这些作用域只能在 Web-aware 的 ApplicationContext (如 XmlWebApplicationContext)中使用。否则会抛出异常。

🔧 前置配置要求:

为了让 Spring 能感知当前请求/会话,必须注册 RequestContextListenerRequestContextFilter

方式一:Listener(推荐)
xml 复制代码
<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>
方式二:Filter(兼容旧容器)
xml 复制代码
<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 请求创建一个实例。
  • 请求处理完后实例被销毁。
xml 复制代码
<bean id="loginAction" class="com.example.LoginAction" scope="request"/>

📌 适用场景:处理请求的控制器辅助类、请求上下文对象。


🧑‍💼 session 作用域
  • 每个用户会话(Session)持有一个实例。
  • 用户退出或 Session 过期时销毁。
xml 复制代码
<bean id="userPreferences" class="com.example.UserPreferences" scope="session"/>

📌 适用场景:用户偏好设置、购物车、登录状态等。


🏢 application 作用域
  • 整个 Web 应用(ServletContext)生命周期内仅一个实例。
  • 类似于 singleton,但它是绑定到 ServletContext 属性上的。
xml 复制代码
<bean id="appConfig" class="com.example.AppConfig" scope="application"/>

📌 适用场景:全局配置、应用级缓存。


🔗 websocket 作用域
  • 每个 WebSocket 连接对应一个 bean 实例。
  • WebSocket 关闭时销毁。
java 复制代码
@Scope("websocket")
@Component
public class ChatHandler { ... }

📌 适用场景:聊天室、实时通知等长连接服务。


🔄 四、关键问题:Singleton Bean 注入 Prototype Bean 怎么办?

这是一个经典陷阱!

❌ 问题描述:

xml 复制代码
<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 配置)
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 类中定义抽象方法:

java 复制代码
public abstract class CommandManager {
    public Object process() {
        MyCommand command = createCommand(); // 每次调用都返回新实例
        return command.execute();
    }

    protected abstract MyCommand createCommand();
}

Spring 会在运行时用 CGLIB 动态生成子类,覆盖 createCommand() 方法,使其每次都返回新的 prototype 实例。


方案 2:使用 ObjectFactoryProvider(推荐,现代写法)
java 复制代码
@Component
public class CommandManager {

    @Autowired
    private ObjectFactory<MyCommand> commandFactory;

    public void process() {
        MyCommand cmd = commandFactory.getObject(); // 每次获取新实例
        cmd.execute();
    }
}

或者使用 JSR-330 的 Provider

java 复制代码
@Inject
private Provider<MyCommand> commandProvider;

public void process() {
    MyCommand cmd = commandProvider.get(); // 每次都是新实例
}

✅ 推荐使用 Provider<T>,类型安全、易于测试。


🔐 五、Scoped Beans 作为依赖时的代理机制(aop:scoped-proxy/)

❓ 问题:为什么要把 session bean 注入 singleton bean 时要用代理?

设想:

xml 复制代码
<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/>

xml 复制代码
<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(...) 时,代理会:
    1. 查找当前线程绑定的 HTTP Session;
    2. 从中取出该用户的 Cart 实例;
    3. 将方法调用委托给真实的 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 接口

java 复制代码
public class ThreadScope implements Scope {
    
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        // 从当前线程获取或创建对象
    }

    @Override
    public Object remove(String name) {
        // 从当前线程移除对象
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // 注册线程结束时的清理逻辑
    }

    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

步骤 2:注册自定义作用域

方式一:编程式注册
java 复制代码
ConfigurableBeanFactory beanFactory = ...;
beanFactory.registerScope("thread", new ThreadScope());
方式二:声明式注册(XML)
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 实时通信

🎯 八、最佳实践建议

  1. 默认使用 singleton:大多数 Bean 都应该是无状态的,适合单例。
  2. 慎用 prototype:确保不会造成资源泄漏,必要时手动清理。
  3. Web 作用域 + 代理 :注入短生命周期 bean 到长生命周期 bean 时,必须使用 <aop:scoped-proxy/>
  4. 优先使用 Provider<T> 替代 lookup method,更现代、更灵活。
  5. 避免覆盖内置 scope :不要尝试重写 singletonprototype
  6. 注意线程安全: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)

示例:

java 复制代码
@SessionScope
@Component
public class UserSessionData {
    // ...
}

或指定代理模式:

java 复制代码
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class Cart { ... }

✅ 总结一句话:

Bean Scope 是 Spring 控制对象生命周期和可见范围的强大机制,通过配置而非硬编码来决定对象的创建方式,极大提升了灵活性和可维护性。

理解了作用域,你就掌握了 Spring 容器如何"按需造物"的秘密。

如需进一步深入,可研究:

  • AOP 代理底层原理(CGLIB vs JDK Dynamic Proxy)
  • ObjectFactoryProvider 的源码实现
  • 自定义 Scope 在分布式上下文传递中的应用(如 TraceId 透传)

希望这份详解能帮你彻底吃透 Spring Bean Scopes!如果你有具体使用场景的问题,也欢迎继续提问。

相关推荐
忧郁的橙子.3 小时前
IntelliJ IDEA 2023中为 Spring Boot 项目添加注释模板
java·spring boot·intellij-idea
IT_陈寒3 小时前
Vue3性能提升30%的秘密:5个90%开发者不知道的组合式API优化技巧
前端·人工智能·后端
秋千码途3 小时前
Spring的@Cacheable取缓存默认实现
java·spring·缓存
聆风吟º3 小时前
【Spring Boot 报错已解决】别让端口配置卡壳!Spring Boot “Binding to target failed” 报错解决思路
android·java·spring boot
我是华为OD~HR~栗栗呀3 小时前
华为od-22届考研-C++面经
java·前端·c++·python·华为od·华为·面试
码住懒羊羊3 小时前
【Linux】操作系统&进程概念
java·linux·redis
我是华为OD~HR~栗栗呀3 小时前
华为OD, 测试面经
java·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀5 小时前
华为OD-23届-测试面经
java·前端·c++·python·华为od·华为·面试
yy7634966685 小时前
WPF 之 简单高效的Revit多语言支持方案
java·大数据·linux·服务器·wpf