文章目录
- 前言
- 一、Bean作用域
-
- [2.1 单例Bean](#2.1 单例Bean)
- [2.2 原型Bean](#2.2 原型Bean)
- [2.3 Web相关Bean作用域](#2.3 Web相关Bean作用域)
- 二、单例Bean中注入原型Bean
- [三、单例Bean也可以 "处理状态"](#三、单例Bean也可以 “处理状态”)
前言
在Spring中,依托于Spring IOC容器,我们可以将对象作为Bean注册到容器里。然后在Spring项目里非常方便的注入进去。那么有个显而易见的问题是每一次通过依赖注入的对象实例都是同一个吗?或者是我们该如何控制对象实例的时机。这篇文章我将梳理一下Bean对象的作用域。
一、Bean作用域
在Spring中,当我们通过@Bean或@Component以及@Component的衍生注解去定义一个Bean的时候,实际上只是相当于定义了一套规则,或者是模板。就像一个类与其各个实例的关系,我们先定义Bean对象,最后依赖注入的时候注入Bean对象的实例也是这种关系。
如何控制从Bean创建的对象的生效范围,这正是我们讨论Bean的作用域。Spring框架支持六种作用域。后四个都是web相关的。其中Spring默认Bean的作用域为单例。
| 作用域 | 描述 |
|---|---|
| singleton(单例) | (默认)为每个 Spring IoC 容器将单个 bean 定义限定为单个对象实例。 |
| prototype(原型) | 将单个 bean 定义限定为任意数量的对象实例。 |
| request(请求) | 将单个 bean 定义限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有其自己的 bean 实例,该实例根据单个 bean 定义创建。仅在 web 感知 Spring ApplicationContext 的上下文中有效。 |
| session(会话) | 将单个 bean 定义限定为 HTTP Session 的生命周期。仅在 web 感知 Spring ApplicationContext 的上下文中有效。 |
| application | application(应用) |
| websocket | 将单个 bean 定义限定为 WebSocket 的生命周期。仅在 web 感知 Spring ApplicationContext 的上下文中有效。 |
2.1 单例Bean
在Spring中我们定义单例Bean的时候我们不需要特殊操作,其默认就是单例的。也就是说这个Bean会随着项目启动被实例化,在往后的每一处依赖注入里注入相同的实例, Spring容器只会返回该一个特定的bean实例。单例Bean适合无状态的组件。
当然也可也手动用@Scope注解声明。
java
@Component
@Scope("singleton")
public class SingletonBean {
}
值得注意的是此处的单例是相对于Spring容器而言,并不是等同于设计模式单例模式中单例模式那种,单例Bean只是Spring容器级别的单例。
单例Bean容器启动时创建,容器销毁时销毁,Spring 管理其完整生命周期。如果想延迟到首次获取时创建可以使用@Lazy注解修饰Bean,功能上类似懒汉式单例。
2.2 原型Bean
对于原型Bean,每次从容器中获取该Bean时,都会创建一个新的实例。容器仅负责创建,不管理生命周期。原型Bean适合有状态的组件。
声明原型Bean需要在@Scope注解中指明prototype。
java
@Component
@Scope("prototype")
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeBean {
}
2.3 Web相关Bean作用域
被定义request作用域的bean,其每个HTTP请求创建一个新实例,请求结束即销毁。适合与单个 HTTP 请求绑定的数据处理。
被定义session作用域的bean,其每个HTTP Session创建一个实例,Session 结束时销毁。适合用户登录信息、会话级缓存等。
被定义application作用域的bean,作用于整个ServletContext,相当于Web应用级别的单例。
被定义websocket作用域的bean,每个WebSocket会话创建一个 Bean 实例。
二、单例Bean中注入原型Bean
在单例作用域bean中使用原型bean依赖项时, 都只注入一个单例Bean实例和原型Bean实例。因为请注意单例作用域的依赖项是在实例化时解析的。因此,如果你将一个原型作用域bean依赖注入到单例作用域 bean 中,一个新的原型 bean 将被实例化,然后依赖注入到单例bean中。由于单例bean只初始化一次,这个原型实例是唯一提供给单例作用域bean的实例。
在这种生命周期不一致的情况下,想控制获取的bean实例。我们可以借助ApplicationContext.getBean()。通过实现ApplicationContextAware接口,通过容器直接获取原型Bean的实例
java
package org.araby.service;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
@Service
public class SingletonBean implements ApplicationContextAware {
private ApplicationContext context; // 持有容器引用
// 每次调用都从容器获取新的原型Bean
public PrototypeBean getPrototypeBean() {
return context.getBean(PrototypeBean.class);
}
// Spring 容器自动注入上下文
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
}
当然也可以使用Spring 内置的@Lookup注解,让Spring自动生成实例,无需手动操作容器。
java
@Component
public class SingletonBean {
// 抽象方法+@Lookup,Spring自动生成实现,每次调用返回新原型实例
@Lookup
public abstract PrototypeBean getPrototypeBean();
}
三、单例Bean也可以 "处理状态"
写到这里可能有人会疑惑:Spring默认Bean为单例,而数据库操作的Service也是单例,没有存储当前请求的状态信息,会不会导致事务回滚失效?
答案是否定的------Spring Boot中事务处理、数据库连接这类"状态",并非存储在Service/Repository/Mapper等单例Bean的实例中,而是通过ThreadLocal实现线程级隔离,再通过上下文传递保证状态的正确性。
这里要明确一个关键区别:"执行有状态的操作" 不等于 "Bean本身是有状态的":
- 有状态的操作:比如数据库事务、请求级的用户身份信息,这类操作需要依赖"上下文状态"完成;
- 有状态的Bean:指Bean实例的成员变量存储"线程共享的可变状态"(如直接用成员变量存用户ID、数据库连接)。
以DAO层数据库操作为例,Service/Mapper(Repository)作为单例Bean,并不会持有固定的数据库Connection实例;而是由Spring事务管理器DataSourceTransactionManager将Connection实例绑定到当前线程的ThreadLocal中,单例Bean仅在执行数据库操作时"调用"该线程专属的Connection。整个过程中,Bean本身无任何可变状态,而Connection的隔离性由ThreadLocal保证,因此事务回滚、连接复用等逻辑都能正常生效。
这一点与ASP.NET Core存在核心差异。ASP.NET Core的DI设计中,控制器、中间件、Service等核心组件默认依赖DI容器注入状态,比如DbContext,每一次HTTP请求都要实例化一次。且DI容器与请求生命周期深度绑定,状态更多通过依赖注入传递而非ThreadLocal隔离;而Spring则通过ThreadLocal将操作状态与单例Bean解耦,既保证单例Bean的性能优势,又通过线程隔离解决状态安全问题。