1. 从一个例子开始
小陈经过开店标准化审计流程后,终于拥有了一家自己的咖啡店,在营业前它向总部的咖啡杯生产工厂订购了一批一次性咖啡杯,每一个用户来到咖啡店可以使用一次性咖啡杯,也可以选择自带最爱的咖啡杯,自带咖啡杯的用户可以享受免费续杯。
这里我们关注三个对象:
-
总部的咖啡杯生产工厂:目前总部只有一家咖啡杯生产工厂,这是单例作用域,只要总部不倒闭,咖啡杯生产工厂一直会存在
-
每一个用户来到咖啡店可以使用的一次性咖啡杯:这是会话作用域,在一次会话(也就是一次到店消费)中这个杯子会一直存在
-
自带最爱的咖啡杯:单例作用域,针对每一个用户来说,它只有一个最爱的咖啡杯。
2. Spring 作用域是什么,有哪些作用域
Spring 作用域是用于定义和管理Bean实例的生命周期范围和共享策略的机制。它决定了Spring容器在何时创建Bean实例、如何共享实例,并在何时销毁实例。通过不同的作用域,Spring能够灵活地适应各种场景需求,例如优化资源使用、隔离用户状态等。
Spring内置了以下常见作用域:
|-------------|--------------------------------------------------|--------------------|
| | | |
| Singleton | 默认作用域,整个容器中仅存在一个Bean实例。 | 数据库连接池、配置类等全局共享组件。 |
| Prototype | 每次请求(如通过getBean()或依赖注入)都创建一个新实例。 | 有状态的工具类(如日期格式化工具)。 |
| Thread | 每一个线程创建一个新实例 | 多线程环境下的状态隔离和线程安全问题 |
| Request | 每个HTTP请求创建一个实例(仅Web应用有效)。 | 用户提交的表单数据、请求级缓存。 |
| Session | 每个用户会话(Session)创建一个实例(仅Web应用有效)。 | 用户登录状态、购物车数据。 |
| Application | 整个ServletContext生命周期内共享一个实例(类似Singleton,但上下文不同)。 | 全局缓存、应用级配置。 |
3. 如何使用这些作用域
Spring中默认bean都是单例,不需要特殊使用注解,或者xml进行标注
3.1 使用注解配置
Java
@Component
@Scope("prototype") // 或 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyPrototypeBean { ... }
- Web作用域专用注解:
Java
@Component
@RequestScope
// 等效于 @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode =ScopedProxyMode.TARGET_CLASS)
public class MyRequestScopedBean { ... }
- Java配置类:
Java
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public MyBean myBean() {
return new MyBean();
}
}
3.2. XML配置
XML
<bean id="myBean" class="com.example.MyBean" scope="prototype"/>
<!-- Web作用域需配置为 request、session 等 -->
<bean id="requestBean" class="com.example.RequestBean" scope="request"/>
4. Spring是如何实现这些作用域的
4.1 解析XML或者注解将Scope记录到BeanDefinition
BeanDefinition:Bean的元数据信息
如同咖啡店开店规格单,就像表格中每一行配置定义了咖啡店的"基因"(用什么机器、找哪家供应商),BeanDefinition是Spring中描述对象的"元数据"。它不关心咖啡机如何运输,只记录"这个对象该长什么样"(类名、属性值、依赖关系等)。
Java
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
/**
* Override the target scope of this bean, specifying a new scope name.
* @see #SCOPE_SINGLETON
* @see #SCOPE_PROTOTYPE
*/
void setScope(@Nullable String scope);
/**
* Return the name of the current target scope for this bean,
* or {@code null} if not known yet.
*/
@Nullable
String getScope();
}
BeanDefintion 提供记录当前Bean Scope以及获取Scope的能力,生成Bean的时候不同的Scope会引导BeanFactory作出不同行为
4.2 单例是如何实现的
java
// Create bean instance.
// mbd是BeanDefinition,如果是单例
if (mbd.isSingleton()) {
// 获取单例,第二个参数是一个函数式接口实例,当beanfactory中没有这个单例的时候调用createBean创建
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
// 省略其他
}
如下是getSingleton的逻辑
可以看到本质上单例对象会使用一个Map进行维护,优先从map中获取,如果没有才会创建bean,并且使用了synchronized来保证并发情况下不会创建多次实例
4.3 原型是如何实现的
如果是原型,每次都会新建一个新的bean
java
// 如果是原型,每次都会新建一个新的bean
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
// 创建实例
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
4.4 其他作用域是如何实现的
java
else {
// 获取作用域名称
String scopeName = mbd.getScope();
// 获取作用域实例
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 调用scope的get方法创建实例
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
其他作用域的bean会调用对于beanDefintion中记录的作用域,然后从scopes中根据scope名称获取对于的Scope对象,然后调用get方法
4.4.1 Scope的接口定义
typescript
// 作用域
public interface Scope {
// 根据名称获取bean,如果bean不存在那么调用ObjectFactory(函数式接口)的getObject进行生成
Object get(String name, ObjectFactory<?> objectFactory);
// 删除bean
@Nullable
Object remove(String name);
// 注册销毁的回调方法
void registerDestructionCallback(String name, Runnable callback);
// 省略其他不关键的方法
}
-
Object get(String name, ObjectFactory<?> objectFactory):根据名称获取bean,如果bean不存在那么调用ObjectFactory(函数式接口)的getObject进行生成
-
Object remove(String name):删除bean
-
void registerDestructionCallback(String name, Runnable callback):注册销毁的回调方法
4.4.2 registerDestructionCallback有什么用
用于管理作用域内 Bean 的生命周期,尤其是在作用域结束时(如请求结束、会话过期、自定义作用域销毁)触发清理逻辑。它在作用域的实现中扮演了 资源释放和生命周期控制 的角色。
比如说http请求的session生命周期结束了,这时候会调用Runnable callback对Session作用域的bean进行清理

DisposableBeanAdapter是一个Runable接口实现,run方法会调用对应的销毁方法@PreDestroy标注的方法DisposableBean#destory方法,以及还会触发DestructionAwareBeanPostProcessor#postProcessBeforeDestruction方法
4.4.3 常见Scope实现
-
thread作用域:实现类SimpleThreadScope,内部使用ThreadLocal来维护bean
-
session作用域:实现类SessionScope,
-
如何复用bean:本质上是从HttpServletRequest中获取Session,然后从Session中获取bean
-
如何销毁bean执行销毁回调:向HttpSession中注册了DestructionCallbackBindingListener(HttpSessionBindingListener)tomcat在session被销毁的时候,会回调valueUnbound方法,在这里会触发bean的销毁
Tomcat Session管理:
- Cookie(默认方式):
检查请求头中的Cookie字段,寻找名为JSESSIONID的Cookie值(可通过context.xml配置自定义名称)。 - URL重写(备用方式):
若Cookie未找到,检查URL中是否包含jsessionid参数(如/path;jsessionid=12345)。
也就是说tomcat会将sessionId写到cookie中,然后浏览器进行存储,并且在服务器本地内存中缓存sessionId对应的HttpSession对象,Spring的Session作用域就是bean存储到HttpSession中
tomcat还会单独启动一个线程,轮训判断session是否过期,如果过期那么调用HttpSessionBindingListener的valueUnbound,从而触发session作用域bean的销毁
-
request作用域:实现类RequestScope
-
如何复用bean:spring mvc会把当前请求存储到threadLocal中,对于request作用的bean其实是从threadLocal中获取当前的HttpServletRequest对象,然后调用getAttribute获取bean对象
-
如何销毁bean执行销毁回调:通过ServletRequestListener的实现类RequestContextListener#requestDestroyed,实现请求完成后Request作用域bean的销毁
-
application作用域:实现类ServletContextScope
-
如何复用bean:优先从ServletContext中进行注册bean,ServletContext在web引用中是唯一的
-
如何销毁bean执行销毁回调:ServletContextScope实现了DisposableBean,spring注册了ContextCleanupListener(ServletContextListener的实现类)在web引用关闭的时候会触发ServletContextListener#contextDestroyed,从而触发ServletContextScope的#destroy,进而触发Applicarion作用域bean的销毁
和单例的区别:单例是先注销单例bean,然后注销Spring的ApplicationContext,Application作用域则是ApplicationContext注销后,再注销Application作用域的bean(取决于ServletContextListener的触发顺序)
5. 为什么说大部分Spring作用域都无用
5.1 单例模式足够高效,适用于99%的场景:
大部分业务逻辑中的 Bean 是无状态的(如 Service、DAO),单例模式可以复用实例、节省资源,天然适合高并发场景。
5.2 存在替代方案
-
显式使用 ThreadLocal:
需要线程级隔离时,开发者可能直接使用 ThreadLocal 存储数据,而非依赖 Spring 的 Thread 作用域,因为前者更轻量且可控。
-
通过参数传递状态:
在方法调用链中传递上下文(如 RequestContextHolder),比依赖作用域 Bean 更直观。
-
复杂作用域的隐性成本
-
配置和维护成本高:
如 request 和 session 作用域需搭配代理模式(ScopedProxyMode),且要确保作用域生命周期与线程、请求或会话同步,容易出错。
-
线程池的复用问题:
使用 thread 作用域时,线程池中的线程可能被复用,残留旧 Bean 状态,需手动清理(如实现 DisposableBean)
5.3 分布式系统的挑战:
在微服务架构中,session 作用域依赖本地会话,无法直接扩展到分布式环境,需借助 Redis 等外部存储,导致作用域 Bean 意义下降。
大型微服务架构中,网络请求需要经过:动态DNS → LVS(四层负载均衡) → 反向代理(七层负载均衡) → 网关(API Gateway) → 应用服务器
一个用户的请求很难做到固定的被同一台机器处理,session,request ,application都是在本地web服务内存维度的作用域,因此在分布式常见下,这些作用域基本上无用
基于redis这样的分布式缓存session,也没有必要把bean存储到redis进行反序列化,来实现session,request,这种作用域
分布式环境中我要尽量保证请求的无状态,无状态利于横向扩容,来提高服务的并发度
6. 有哪些巧妙的设计
- 面向接口编程:
Spring将作用域抽象为Scope接口,在接口中定义了作用域的能力,支持开发者根据自己的需要可插拔的加入自己的作用域
- 监听器模式:
对于web服务中的作用域,Spring结合javaee中提供的扩展Listener,实现对应作用域对象销毁的回调逻辑,例如基于HttpSessionBindingListener、RequestContextListener、ServletContextListener,分别实现了session,request,application三种作用域的对象销毁回调
7. 面试题&总结
7.1 Spring中有哪些作用域、各自有哪些应用场景
|-------------|--------------------------------------------------|--------------------|
| | | |
| Singleton | 默认作用域,整个容器中仅存在一个Bean实例。 | 数据库连接池、配置类等全局共享组件。 |
| Prototype | 每次请求(如通过getBean()或依赖注入)都创建一个新实例。 | 有状态的工具类(如日期格式化工具)。 |
| Thread | 每一个线程创建一个新实例 | 多线程环境下的状态隔离和线程安全问题 |
| Request | 每个HTTP请求创建一个实例(仅Web应用有效)。 | 用户提交的表单数据、请求级缓存。 |
| Session | 每个用户会话(Session)创建一个实例(仅Web应用有效)。 | 用户登录状态、购物车数据。 |
| Application | 整个ServletContext生命周期内共享一个实例(类似Singleton,但上下文不同)。 | 全局缓存、应用级配置。 |
这里需要自主的提出,除了单例和原型基本上都没啥用,单例有极致的性能并且大部分业务代码中的service,dao都是无状态的,并且在分布式环境中网络请求需要经过:动态DNS → LVS(四层负载均衡) → 反向代理(七层负载均衡) → 网关(API Gateway) → 应用服务器
一个用户的请求很难做到固定的被同一台机器处理,session,request ,application都是在本地web服务内存维度的作用域,因此在分布式常见下,这些作用域基本上无用
分布式环境中我要尽量保证请求的无状态,无状态利于横向扩容,来提高服务的并发度
7.2 Spring中的作用域底层原理是什么
见4. Spring是如何实现这些作用域的
伏笔
单例AService 调用原型的BService,每一次是同一个BService,还是不同的BService
注意AService的属性引用了BService的实例,还能做到BService是原型么?
java
@Component
public class AService {
@Autowired
private BService bService;
// 单例AService 调用原型的BService,每一次是同一个BService,还是不同的BService
public void DoSomething() {
bService.DoSomething();
}
}
@Component
@Scope("prototype")
class BService {
public void DoSomething() {
}
}
。