开篇引入
在Spring框架中,Bean的作用域(Scope)是控制对象生命周期和可见范围的关键机制,也是面试中绕不开的高频考点-25。很多开发者用了Spring好几年,谈起@Scope却只会说“单例就是全局一个,原型就是每次新建”。遇到“prototype Bean注入到singleton Bean后为什么失效”、“为什么多个请求互相污染数据”这类实际问题时,往往一头雾水。本文将结合原理剖析、代码示例和高频面试题,帮你彻底搞懂Bean作用域。

一、痛点切入:为什么需要Bean作用域
先看一个传统做法。早期Java Web开发中,对象创建全由开发者手动控制:

// 传统做法:手动管理对象实例 public class OrderService { private ShoppingCart cart; public void processOrder(Long userId) { // 每次都要手动new this.cart = new ShoppingCart(); // 问题:购物车在方法内局部有效,无法跨多个请求保持用户状态 } }
这段代码的痛点很明显:
状态管理困难:用局部变量,用户会话状态无法跨请求保持;用全局变量,多用户数据会互相污染。
耦合高:业务代码与对象创建方式强绑定,难以切换。
生命周期控制混乱:对象的创建和销毁散落在各业务方法中,维护成本高。
Spring的Bean作用域机制正是为了解决这些问题而设计的。
二、核心概念讲解:什么是Bean作用域
Bean作用域(Scope) :同一个BeanDefinition被Spring容器创建对象时,创建几个实例、这些实例在什么范围内被复用的规则-8。
用人话说就是:告诉Spring,这个Bean“住多久”“给谁用” 。
singleton(单例) :就像公司里的打印机——全公司共用一台,谁用都是同一台。每个Spring IoC容器中,同名称的Bean只有一个实例,所有依赖注入引用的都是同一个对象-8。
prototype(原型) :就像一次性的纸杯——每次招待客人拿一个新杯子。每次获取Bean时都创建一个新实例-2。
request(请求级) :就像快递站的取件码——每个包裹(HTTP请求)有自己的独立编号,取完即失效。
session(会话级) :就像酒店房卡——同一住客(用户会话)多次进出都用同一张卡,退房(会话结束)时销毁-25。
一句话总结:作用域就是Bean的“居住证”——规定它在容器里住多久、和谁合住、住几个人。
三、关联概念讲解:singleton vs prototype
Singleton(单例)——Spring的默认作用域
标准定义:每个Spring IoC容器中,指定名称的Bean只有一个实例,所有对该Bean的请求都返回同一个对象引用-8。
// 默认就是singleton,@Scope("singleton")可省略 @Component public class UserService { public void doSomething() { / 无状态业务逻辑 / } }
核心特点:
容器启动时创建(饿汉式,可通过
@Lazy改为懒加载)生命周期与容器绑定——容器关闭时销毁-8
适合无状态的Service、DAO、工具类
Prototype(原型)——每次获取都新建
标准定义:每次从容器中获取Bean时,都会创建一个全新的实例。Spring只负责创建,不管理其完整生命周期-2。
@Component @Scope("prototype") public class ShoppingCart { private List<Item> items = new ArrayList<>(); // 有状态:每个用户拥有独立的购物车 }
核心特点:
每次调用
getBean()都创建新实例不自动执行
@PreDestroy等销毁方法,资源清理需开发者手动处理-35适合有状态的对象(如购物车、上下文对象)
四、概念关系与区别总结
| 维度 | Singleton | Prototype |
|---|---|---|
| 实例数量 | 容器内仅1个 | 每次获取都新建 |
| 生命周期 | 容器管理(启动创建→关闭销毁) | Spring只负责创建,销毁需手动 |
| 线程安全 | 需自行保证 | 天然隔离(每线程独立实例) |
| 内存占用 | 极低(仅1份) | 较高(频繁创建) |
| 适用场景 | 无状态Service/DAO/工具类 | 有状态对象(购物车/上下文) |
一句话记忆:Singleton是“全公司共用一台打印机”,prototype是“每人领一个新水杯”-35。
五、代码示例:直观对比两种作用域
// 1. 定义Singleton Bean(默认) @Component public class SingletonBean { public SingletonBean() { System.out.println("创建SingletonBean:" + this.hashCode()); } } // 2. 定义Prototype Bean @Component @Scope("prototype") public class PrototypeBean { public PrototypeBean() { System.out.println("创建PrototypeBean:" + this.hashCode()); } } // 3. 测试 @SpringBootApplication public class ScopeDemo implements CommandLineRunner { @Autowired private ApplicationContext context; @Override public void run(String... args) { // 两次获取Singleton Bean System.out.println("=== Singleton Bean ==="); context.getBean(SingletonBean.class); context.getBean(SingletonBean.class); // 两次获取Prototype Bean System.out.println("=== Prototype Bean ==="); context.getBean(PrototypeBean.class); context.getBean(PrototypeBean.class); } }
执行输出:
创建SingletonBean:394827368 === Singleton Bean === === Singleton Bean === 创建PrototypeBean:827461952 创建PrototypeBean:561028315
发生了什么:
Singleton Bean:容器启动时创建一次,后续获取都是同一个实例。
Prototype Bean:每次调用
getBean()都创建新实例,hashCode不同-8-38。
六、底层原理:作用域如何实现
Spring的底层实现依赖两个关键机制:
1. Scope接口
所有作用域通过org.springframework.beans.factory.config.Scope接口统一抽象,定义了get()、remove()等核心方法。容器通过判断BeanDefinition中指定的作用域类型,决定实例的创建和获取策略-25。
2. 代理模式 + AOP
@Scope注解中的proxyMode属性是关键。当singleton Bean注入prototype Bean时,Spring并非直接注入prototype实例,而是注入一个代理对象。每次调用代理对象的方法时,代理会从容器中获取最新的prototype实例。如果缺少这层代理,prototype Bean注入到singleton Bean后会被“固化”为单例-35。
依赖支撑:作用域机制的底层依赖Spring的核心能力——BeanFactory容器管理 + 反射 + 动态代理。容器接管了对象的创建、依赖注入、销毁等全流程,底层靠“反射 + 设计模式”实现-45。
七、常见陷阱与解决方案
陷阱一:prototype Bean注入到singleton后失效
问题:prototype Bean被注入到singleton Bean时,会被“固化”,每次调用的都是同一个实例。
解决方案:使用@Lookup方法注入。
@Component public class SingletonService { // 方法注入:每次调用getPrototypeBean()都获取新实例 @Lookup public PrototypeBean getPrototypeBean() { return null; // Spring会动态实现此方法 } public void doSomething() { PrototypeBean bean = getPrototypeBean(); // 每次都新建 bean.process(); } }
陷阱二:singleton Bean线程安全问题
问题:singleton Bean被多线程共享,若包含可变成员变量,会出现线程安全问题。
// ❌ 错误示例:有状态的Singleton Bean @Service public class OrderProcessor { private String currentUser; // 危险!共享可变状态 public void processOrder(Long orderId) { this.currentUser = getCurrentUser(); // 多线程相互覆盖! } }
解决方案:
使用局部变量(推荐):将状态作为方法参数传递。
使用ThreadLocal:隔离线程状态,但务必在请求结束时清理。
改用prototype/request作用域:对需要独立状态的组件,声明为非单例-10-13。
八、高频面试题与参考答案
1. Spring支持哪些Bean作用域?默认是什么?
参考答案:Spring支持singleton(默认)、prototype、request、session、application、websocket六种作用域。其中singleton为默认作用域,在非Web环境下只前两种可用-38。
2. singleton和prototype的核心区别是什么?
参考答案:
实例数量:singleton在整个容器中只有一个实例;prototype每次获取都创建新实例。
生命周期管理:singleton由容器全权管理(创建到销毁);prototype容器只负责创建,不管理销毁,
@PreDestroy等方法不会自动执行-38。
3. 为什么prototype Bean注入到singleton Bean后“失效”?
参考答案:Spring容器在初始化singleton Bean时,会一次性完成所有依赖注入,包括prototype依赖。此后singleton Bean始终持有最初创建的prototype实例,不再重新获取。解决方法是使用@Lookup方法注入或ObjectFactory延迟获取-35-26。
4. singleton Bean是线程安全的吗?如何保证?
参考答案:Spring不保证singleton Bean的线程安全——Spring只负责实例唯一,不干预业务逻辑的并发执行。保证方法有三种:①使用无状态设计(不定义可变成员变量);②使用局部变量替代成员变量;③使用ThreadLocal隔离状态-13-10。
5. Spring的默认作用域是单例,这和设计模式中的单例模式一样吗?
参考答案:不一样。Spring的singleton是“容器级别”单例——每个IoC容器一个实例,不同容器可拥有不同实例;而设计模式的单例模式是“类加载器级别”单例,一个JVM内一个类仅有一个实例-8。
九、结尾总结
本文围绕Spring Bean作用域的核心知识点进行了系统梳理:
| 核心知识点 | 关键要点 |
|---|---|
| 作用域概念 | 决定Bean实例的创建数量与共享范围 |
| Singleton | 默认作用域,容器内唯一实例,适合无状态服务 |
| Prototype | 每次获取新建,需自行管理销毁 |
| 底层原理 | Scope接口 + 代理模式 + 反射 + IoC容器 |
| 线程安全 | Spring不保证,需开发者自行设计 |
| 高频面试 | 六大作用域 + 区别 + 失效场景 + 线程安全 |
重点与易错点:
singleton是容器级别单例,不是JVM级单例
prototype注入到singleton会“固化”,需用
@Lookup解决Spring不保证singleton的线程安全,无状态设计是推荐做法
预告:下一篇将深入探讨Spring Bean的完整生命周期——从实例化到销毁的每一步,结合BeanPostProcessor和Aware接口,助你彻底搞懂Spring容器背后的运作机制。
本文知识点更新至2026年4月,适用于Spring 5.x/6.x版本。代码基于Spring Boot 3.x环境编写。