【Spring6笔记】 - 3 - Bean的作用域
本文详细介绍了Spring框架中Bean的两种核心作用域:Singleton(单例)和Prototype(原型)。Singleton是默认作用域,容器内仅维护一个实例,具有预实例化特性;而Prototype每次请求都会创建新实例,采用延迟初始化策略。文章通过代码示例对比了两者在实例化时机、线程安全性、生命周期管理等方面的差异,并特别分析了Singleton依赖Prototype时的作用域失效问题及
【Spring6笔记】 - 3 - Bean的作用域
1. Singleton Scope(单例作用域)
singleton 是 Spring 容器默认的作用域。在每一个 Spring IoC 容器(如 ClassPathXmlApplicationContext 实例)中,同名的 Bean 定义只会对应一个唯一的实例对象。
1.1 核心特征
- 唯一性:在整个容器生命周期内,该 Bean 只有一个对象。
- 共享性:所有对该 Bean 的
getBean()调用或依赖注入,指向的都是同一个内存地址。 - 默认性:如果
<bean>标签中不写scope属性,默认就是singleton。
1.2 实例化时机与策略
根据测试代码和 Spring 源码机制,singleton 的生命周期有以下特点:
- 预实例化(Pre-instantiation):默认情况下,当你执行
new ClassPathXmlApplicationContext(...)初始化容器时,Spring 就会立即创建所有的单例 Bean。- 验证方法:可以在
SpringBean的无参构造方法里打印一句话。你会发现,还没调用getBean(),控制台就已经打印了。
- 验证方法:可以在
- 延迟加载(Lazy Init):如果你希望在第一次调用
getBean()时才创建,可以在 XML 中配置lazy-init="true"。
1.3 代码实战分析
结合你提供的测试代码 testScope(),我们可以总结出以下结论:
XML 配置:
<bean id="springBean" class="com.zzz.bean.SpringBean" scope="singleton"/>
测试结论:
SpringBean springBean1 = applicationContext.getBean("springBean", SpringBean.class);
SpringBean springBean2 = applicationContext.getBean("springBean", SpringBean.class);
// 结果为 true,证明两次获取的是同一个对象(内存地址一致)
System.out.println(springBean1 == springBean2);
1.4 深度原理:单例池(Singleton Objects)
Spring 内部通过一个所谓的“一级缓存”来保证单例。其本质是一个 ConcurrentHashMap:
- Key:Bean 的名字(如
"springBean")。 - Value:Bean 的实例对象。
- 流程:当调用
getBean时,Spring 先去 Map 里看有没有。有则直接返回;没有则创建、注入依赖、放入 Map、最后返回。
1.5 注意事项:线程安全问题
这是单例模式下最需要关注的开发痛点!
- 无状态 Bean(Thread-Safe):绝大多数的
Service和DAO属于此类。它们只负责逻辑处理,不存储成员变量数据。在多线程环境下是安全的,推荐使用单例。 - 有状态 Bean(Not Thread-Safe):如果
SpringBean中定义了一个private int count;变量,多个线程同时去修改这个count,就会发生数据冲突。- 解决方案:
- 将 Scope 改为
prototype。 - 使用
ThreadLocal(如你代码中展示的SimpleThreadScope思路)。
- 将 Scope 改为
- 解决方案:
1.6 与 Java 设计模式中单例的区别
| 维度 | Java 原生单例 (Singleton Pattern) | Spring 单例 Bean |
|---|---|---|
| 控制权 | 程序员手动实现(私有构造、静态方法) | 由 Spring 容器负责生命周期管理 |
| 范围 | 整个 ClassLoader(类加载器)唯一 | 每一个 Spring IoC 容器 内唯一 |
| 灵活性 | 较差,硬编码 | 极强,通过 XML 或注解轻松切换 |
2. Prototype Scope(原型/多例作用域)
prototype(原型模式)与 singleton 完全相反。每当向 IoC 容器请求该 Bean 时,Spring 都会新建一个实例。
2.1 核心特征
- 多例性:每次调用
getBean(),Spring 都会执行一次new操作。 - 独立性:每个通过容器获取的对象都有独立的内存地址,互不影响。
- 显式配置:必须在 XML 中明确设置
scope="prototype"。
2.2 实例化时机(关键区别)
不同于单例的“预实例化”,原型 Bean 采用的是 “延迟初始化” 策略:
- 启动阶段:当
new ClassPathXmlApplicationContext(...)执行时,Spring 不会创建scope="prototype"的 Bean。 - 运行阶段:只有当你显式调用
applicationContext.getBean("springBean")时,容器才会实时创建该对象。
2.3 代码实战分析
结合测试类 SpringScopeTest 中的 testScope() 方法,我们可以得出以下实战结论:
XML 配置:
<bean id="springBean" class="com.zzz.bean.SpringBean" scope="prototype"/>
测试验证逻辑:
// 第一次获取,Spring new 了一个对象 A
SpringBean springBean1 = applicationContext.getBean("springBean", SpringBean.class);
// 第二次获取,Spring 又 new 了一个对象 B
SpringBean springBean2 = applicationContext.getBean("springBean", SpringBean.class);
// 结果为 false,证明这是两个完全不同的对象
System.out.println(springBean1 == springBean2);
2.4 深度原理:只管生,不管养
这是 prototype 最特殊的一点:Spring 不负责原型 Bean 的完整生命周期。
- 创建阶段:容器会帮其实例化、配置、装饰并初始化。
- 销毁阶段:一旦 Bean 交给了客户端(调用者),Spring 容器就不再持有这个对象的引用,也不会调用它的
destroy方法。 - 后果:原型 Bean 的资源释放(如关闭流、清理内存)必须由程序员手动处理,否则频繁调用可能导致内存泄漏。
2.5 线程安全
- 天然线程安全:因为每个线程请求时都可以获取一个新的实例,不存在多个线程共享同一个对象成员变量的问题。
2.6 适用场景
- 有状态的 Bean:比如一个
User实体类,或者包含用户特定信息的Action/Controller(在某些老框架中)。 - 非共享资源:每次使用都需要干净状态的对象。
3. Singleton vs Prototype
| 特性 | Singleton(单例) | Prototype(原型) |
|---|---|---|
| 实例数量 | 容器内仅一个 | 每次请求创建一个新实例 |
| 创建时机 | 容器启动时(默认) | 调用 getBean() 时 |
| Bean 存储 | 存在 Spring 一级缓存(单例池)中 | 容器不存储,直接交给调用者 |
| 生命周期 | 容器负责全过程(初始化到销毁) | 容器只负责创建和初始化 |
| 性能 | 高(对象复用) | 相对较低(频繁创建对象) |
4. 作用域失效问题:当 Singleton 依赖 Prototype
如果你有一个单Singleton 的 Service 注入了一个Prototype 的 User,会发生什么?
- 结论:由于
Service只初始化一次,它持有的User也只会被注入一次。这意味着,虽然User配置成了prototype,但在该Service中它表现得像单例。 - 解决方法:使用
Lookup Method Injection(查找方法注入)。
1. 现象还原(为什么会失效?)
假设我们有一个单例的 OrderService,每处理一个订单都需要一个新的 Cart(原型)对象。
Java 代码:
// 原型 Bean
public class Cart {
public Cart() { System.out.println("Cart 实例已创建!"); }
}
// 单例 Bean
public class OrderService {
private Cart cart; // 期待每次使用时都是新的
public void setCart(Cart cart) { this.cart = cart; }
public void processOrder() {
System.out.println("使用购物车实例: " + cart);
}
}
XML 配置:
<bean id="cart" class="com.zzz.bean.Cart" scope="prototype"/>
<bean id="orderService" class="com.zzz.bean.OrderService" scope="singleton">
<property name="cart" ref="cart"/>
</bean>
测试结论: 当你多次调用 orderService.processOrder() 时,你会发现控制台打印的 cart 地址始终是一样的。这是因为 orderService 只实例化了一次,cart 也只被注入了一次。
2. 解决方法:Lookup Method Injection(查找方法注入)
Spring 提供的 lookup-method 可以通过 CGLIB 动态代理技术,在运行时重写单例 Bean 的某个方法,让该方法每次去容器中重新请求目标 Bean。
第一步:修改 Java 类
将单例 Bean 定义为一个抽象类,并定义一个抽象方法来获取原型 Bean。
public abstract class OrderService {
public void processOrder() {
// 注意:这里不再使用成员变量,而是调用抽象方法
Cart cart = getCart();
System.out.println("获取到新的购物车实例: " + cart);
}
// 定义一个抽象方法,Spring 会负责实现它
public abstract Cart getCart();
}
第二步:修改 XML 配置
使用 <lookup-method> 标签告知 Spring:当你调用 getCart 方法时,请去容器里找名为 "cart" 的 Bean。
<bean id="cart" class="com.zzz.bean.Cart" scope="prototype"/>
<bean id="orderService" class="com.zzz.bean.OrderService" scope="singleton">
<lookup-method name="getCart" bean="cart"/>
</bean>
第三步:运行结果
此时,每当你调用 orderService.processOrder(),Spring 代理对象都会拦截 getCart() 方法,并执行类似 context.getBean("cart") 的操作。你会发现:
- 控制台会再次打印 “Cart 实例已创建!”。
- 每次拿到的
cart对象内存地址都不一样了。
3. 其他替代方案
除了 lookup-method,在现代开发(注解驱动)中,还有以下两种常用方法:
-
注入 ApplicationContext: 让
OrderService实现ApplicationContextAware接口,手动在方法内部调用context.getBean("cart")。- 缺点:代码与 Spring API 强耦合,违背了 IoC 思想。
-
使用
@Lookup注解(推荐的注解方式):@Component public class OrderService { @Lookup public Cart getCart() { return null; // Spring 会重写此方法,返回原型 Bean } }
5. 其他 Scope(Web & 自定义)
除了 singleton 和 prototype,Spring 还提供了 5 种专门用于 Web 环境的作用域,以及支持开发者根据需求扩展的自定义作用域。
5.1 Web 相关作用域(仅限 Web 应用)
这些作用域在标准的 Java 应用程序(如简单的 Swing 或控制台程序)中不可用。只有在使用了 Spring MVC 或类似 Web 框架的环境下,且配置了 RequestContextListener 后才能生效。
| 作用域名称 | 生命周期描述 | 适用场景 |
|---|---|---|
| request | 每一个 HTTP 请求都会创建一个新的 Bean 实例。请求结束后,Bean 随之销毁。 | 存储单个请求的临时数据(如表单校验错误)。 |
| session | 在同一个 HTTP Session(会话)中,该 Bean 是单例的。 | 存储用户信息、购物车等需要跨请求保持的数据。 |
| application | 在整个 ServletContext 生命周期内是一个单例。 |
类似于 singleton,但它是 Servlet 容器级别的全局共享。 |
| websocket | 在一个 WebSocket 的生命周期内,该 Bean 是单例的。 | 实时通信中的用户连接状态管理。 |
| global session | (已过时)原用于 Portlet 环境,目前在普通 Web 开发中等同于 session。 | 早期 Portlet 应用规范。 |
5.2 自定义 Scope:ThreadScope(线程作用域)
ThreadScope 是一个非常经典的非标准作用域。它保证了同一个线程内获取的是同一个对象,不同线程获取的是不同对象。
1. XML 配置实现
要使用非内置作用域,必须先向 Spring 注册。配置如下:
<bean id="springBean" class="com.zzz.bean.SpringBean" scope="threadScope"/>
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="threadScope">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
2. 测试代码逻辑分析
通过testThreadScope() 方法,我们可以验证其核心逻辑:
public void testThreadScope(){
// 主线程(Thread A)获取
SpringBean b1 = applicationContext.getBean("springBean", SpringBean.class);
SpringBean b2 = applicationContext.getBean("springBean", SpringBean.class);
System.out.println(b1 == b2); // 结果为 true:同一线程内是单例
// 启动一个新线程(Thread B)
new Thread(() -> {
SpringBean b3 = applicationContext.getBean("springBean", SpringBean.class);
System.out.println(b3);
// 结果:b3 的内存地址与 b1/b2 不同,因为跨线程了
}).start();
}
3. 底层原理:ThreadLocal
SimpleThreadScope 的底层原理是利用了 Java 的 ThreadLocal。它为每个线程准备了一个独立的“盒子”,线程 A 只能从自己的盒子里拿对象,线程 B 亦然。
5.3 如何自定义一个 Scope?
如果你想完全自己实现一个 Scope(例如:按“地理位置”划分的作用域),你需要:
- 实现
org.springframework.beans.factory.config.Scope接口:- 实现
get(String name, ObjectFactory<?> objectFactory):定义如何获取/创建对象。 - 实现
remove(String name):定义销毁逻辑。
- 实现
- 注册到容器:使用
CustomScopeConfigurer进行配置。
1. 第一步:编写自定义 Scope 类
我们需要实现 org.springframework.beans.factory.config.Scope 接口。核心在于使用一个 ThreadLocal 来保存每个线程自己的 Bean 副本。
package com.zzz.scope;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义线程作用域实现
*/
public class MySimpleThreadScope implements Scope {
// 使用 ThreadLocal 存储当前线程的所有 Bean 实例
private final ThreadLocal<Map<String, Object>> threadLocal = ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 1. 获取当前线程的 Bean 容器(Map)
Map<String, Object> scope = threadLocal.get();
// 2. 检查 Map 中是否已经存在该 Bean
Object bean = scope.get(name);
if (bean == null) {
// 3. 如果不存在,调用 ObjectFactory 创建 Bean 实例
bean = objectFactory.getObject();
// 4. 存入当前线程的 Map 中
scope.put(name, bean);
}
return bean;
}
@Override
public Object remove(String name) {
// 从当前线程的 Map 中移除 Bean
return threadLocal.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 这里可以实现销毁回调逻辑,简单实现可以先忽略
System.out.println("注册销毁回调: " + name);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
// 返回当前线程的名字作为会话 ID
return Thread.currentThread().getName();
}
}
2. 第二步:在 XML 中注册并使用
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="myThread">
<bean class="com.zzz.scope.MySimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="myBean" class="com.zzz.bean.SpringBean" scope="myThread"/>
</beans>
3. 第三步:编写 JUnit 测试用例
我们要通过多线程环境来验证:同一个线程拿到的是同一个对象,不同线程拿到的是不同对象。
import com.zzz.bean.SpringBean;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyCustomScopeTest {
@Test
public void testMyCustomScope() throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-custom-scope.xml");
// --- 线程 1 (主线程) ---
SpringBean b1 = context.getBean("myBean", SpringBean.class);
SpringBean b2 = context.getBean("myBean", SpringBean.class);
System.out.println("主线程 Bean1: " + b1);
System.out.println("主线程 Bean2: " + b2);
System.out.println("主线程两次获取是否相同: " + (b1 == b2)); // true
// --- 线程 2 (新线程) ---
Thread thread2 = new Thread(() -> {
SpringBean b3 = context.getBean("myBean", SpringBean.class);
SpringBean b4 = context.getBean("myBean", SpringBean.class);
System.out.println("新线程 Bean3: " + b3);
System.out.println("新线程 Bean4: " + b4);
System.out.println("新线程内部获取是否相同: " + (b3 == b4)); // true
System.out.println("跨线程获取是否相同 (b1 vs b3): " + (b1 == b3)); // false
});
thread2.start();
thread2.join(); // 等待子线程运行结束
}
}
测试结果:
调用链条如下:调用链条如下:
- 测试代码:执行
context.getBean("myBean")。 - Spring 容器(BeanFactory):
- 查找
myBean的定义,发现它的scope="myThread"。 - 容器去它的“作用域注册表”里找:
"myThread" 对应哪个实现类? - 找到了你注册的
MySimpleThreadScope实例。
- 查找
- 关键转折点:Spring 容器不再自己去创建对象,而是把创建权转交给自定义 Scope。
核心方法被调用的时机
(1) get(String name, ObjectFactory<?> objectFactory)
- 调用时机:当你调用
context.getBean()且 Bean 的作用域是自定义的时候。 - Spring 的逻辑:
- Spring 会把创建 Bean 的逻辑封装成一个
ObjectFactory(工厂对象)。 - Spring 调用
scope.get(...),并把这个工厂传给你。 - 由你决定:是直接从
ThreadLocal拿旧的(如果你已经存过了),还是调用objectFactory.getObject()创建一个新的。
- Spring 会把创建 Bean 的逻辑封装成一个
(2) remove(String name)
- 调用时机:通常在作用域显式结束时。
- 注意:对于
ThreadScope或自定义作用域,Spring 不会自动知道线程什么时候结束。 - 手动调用:通常需要你在代码中(例如 Web 拦截器或线程池任务结束时)手动调用
configurableBeanFactory.getRegisteredScope("myThread").remove("myBean")来清理内存。
(3) registerDestructionCallback(...)
- 调用时机:在 Bean 实例创建完成并初始化后,Spring 会自动调用此方法。
- 作用:Spring 会传给你一个
callback(其实就是 Bean 的销毁方法,如@PreDestroy)。 - 责任:因为是自定义作用域,Spring 无法预知这个 Bean 什么时候该死。我们需要把这个
callback存起来,等到我们觉得该销毁这个作用域的时候(比如线程销毁),由我们手动运行这个callback。
(4) getConversationId()
- 调用时机:Spring 内部记录日志或进行调试统计时调用。
- 作用:用来标识当前的“会话”到底是谁。返回线程名,这样 Spring 打印日志时就知道这个 Bean 属于哪个线程。
用一个生动的例子来理解
想象你在一家**定制化餐厅(Spring)**吃饭:
- 你(测试代码):对服务员说“我要一份‘myBean’”。
- 服务员(Spring 容器):看了一下菜单,发现这个菜标注了“由‘myThread’大厨负责”。
- 服务员:把点菜单和原材料交给了 大厨( MySimpleThreadScope)。
- 大厨(执行
get方法):- 大厨先看了一下自己灶台上的备菜盘(
ThreadLocal)。 - 发现盘子里已经有一份做好的了,直接拿给服务员(返回缓存对象)。
- 如果盘子是空的,大厨才现场开火把菜做出来(执行
objectFactory.getObject()),然后放进盘子备份。
- 大厨先看了一下自己灶台上的备菜盘(
流程总结:
- 配置注册:通过
CustomScopeConfigurer将自定义 Scope 注入 Spring 的 Scope 映射表。 - 拦截请求:
AbstractBeanFactory.doGetBean方法会判断 Bean 的 Scope 类型。 - 委托执行:如果是非单例、非原型的自定义 Scope,Spring 调用
scope.get(name, objectFactory)。 - 策略实现:开发者在
get方法中利用ThreadLocal、Redis或其他容器实现特定的存储逻辑。
深度原理解析
- ObjectFactory 的作用:在
get方法中,objectFactory.getObject()是关键。Spring 容器并不会直接把 Bean 交给 Scope,而是把“生产 Bean 的工厂”交给 Scope。由 Scope 决定什么时候调用工厂去生产(如果是第一次访问就生产,如果已经有了就直接从 Map 拿缓存)。- ThreadLocal 隔离:每个线程在调用
threadLocal.get()时,拿到的都是属于该线程独立的HashMap。这就是为什么不同线程之间的 Bean 互不干扰。- 注册机制:
CustomScopeConfigurer实际上是告诉 Spring 的BeanFactory:“以后如果你看到scope="myThread",就去调用我提供的这个MySimpleThreadScope实例来处理。”
更多推荐



所有评论(0)