一、引言
Spring 的核心是 IoC 容器,而 IoC 容器最核心的"成员"就是 Bean。很多人刚学 Spring 时,会被"生命周期"和"作用域"两个概念绕晕:
- 生命周期讲的是 Bean 从创建到销毁的全过程。
- 作用域讲的是 Bean 存在的范围与数量。
实际上,二者必须结合起来理解------不同作用域的 Bean,生命周期行为差异巨大。
本文将带你彻底搞懂:
- Singleton 作用域下的完整生命周期(8 个阶段)。
- Prototype 作用域为什么"只管生不管死"。
- Request / Session 作用域的特殊之处。
- 不同作用域混合注入时的注意事项。
二、Spring 所有作用域总览
Spring 共支持 6 种内置作用域(其中前两种在任何环境中可用,后四种仅在 Web 环境中可用):
| 作用域 | 说明 | 创建时机 | 销毁时机 | 是否需代理 |
|---|---|---|---|---|
| singleton | 容器级别单例 | 容器启动(或懒加载时) | 容器关闭 | 否 |
| prototype | 每次获取都创建新实例 | 每次调用 getBean() | 容器不管理,需手动 | 否 |
| request | 每个 HTTP 请求一个实例 | 每个请求到达时 | 请求结束 | 是 |
| session | 每个 HTTP Session 一个实例 | 第一次请求时创建 Session | Session 过期/销毁 | 是 |
| application | ServletContext 级别单例 | ServletContext 初始化时 | 应用关闭 | 是 |
| websocket | 每个 WebSocket 会话一个实例 | WebSocket 连接建立时 | 连接关闭 | 是 |
此外,Spring Cloud 等生态还提供了自定义作用域(如 refresh scope),本文先聚焦内置作用域。
三、标准 Bean 生命周期(以 Singleton 为例)
Singleton 是 Spring 默认作用域,也是生命周期最完整的代表。 整个过程由 Spring 容器全权管理。
完整阶段一览(建议对照下文理解)
text
实例化 → 属性注入 → Aware 回调 → 初始化前 → 自定义初始化 → 初始化后 → 使用中 → 销毁
1. 实例化(Instantiation)
- 通过构造器(或工厂方法)创建 Bean 实例。
- 此时对象已存在,但属性还未注入。
2. 属性注入(Populate Properties)
- Spring 根据
@Autowired、@Resource、@Value等注解,或者 XML 配置,完成依赖注入。
3. Aware 接口回调
如果 Bean 实现了以下 Aware 接口,Spring 会回调对应方法:
| Aware 接口 | 回调时机 | 用途 |
|---|---|---|
BeanNameAware |
注入 Bean 在容器中的名称 | 获取自己的 Bean 名字 |
BeanClassLoaderAware |
注入类加载器 | 动态加载类 |
BeanFactoryAware |
注入当前 BeanFactory |
手动获取其他 Bean |
EnvironmentAware |
注入环境变量 | 读取配置 |
ApplicationContextAware |
注入 ApplicationContext |
获取更丰富的容器能力 |
4. 初始化前(BeanPostProcessor.before)
- 调用所有注册的
BeanPostProcessor的postProcessBeforeInitialization方法。 - 常见应用:对 Bean 做前置增强、校验。
5. 自定义初始化(三选一或多选)
Spring 按以下顺序执行(可同时存在,但建议只用一种):
@PostConstruct注解的方法InitializingBean.afterPropertiesSet()@Bean(initMethod = "myInit")指定的方法
6. 初始化后(BeanPostProcessor.after)
- 调用
postProcessAfterInitialization。 - 这是 AOP 生成代理对象的常见时机 (如
@Transactional在这里生效)。
7. 使用中
- Bean 已经就绪,可以被业务代码调用。
8. 销毁(容器关闭时)
Spring 按以下顺序执行:
@PreDestroy注解的方法DisposableBean.destroy()@Bean(destroyMethod = "myDestroy")指定的方法
四、6 大作用域详解(逐个拆解)
1. Singleton(单例作用域)
这是 Spring 的默认作用域。
特征
- 整个 Spring 容器中,该 Bean 只有一个实例。
- 所有使用该 Bean 的地方,共享同一个对象。
生命周期
- 创建 :容器启动时立即创建(除非加
@Lazy懒加载)。 - 初始化:完整走完 1~6 步。
- 销毁:容器正常关闭时,走第 8 步。
- 结论:所有生命周期扩展点都生效,Spring 全权管理。
代码示例
java
@Component
@Scope("singleton") // 可省略,因为默认就是 singleton
public class SingletonBean {
public SingletonBean() {
System.out.println("SingletonBean 实例化");
}
}
适用场景
- 无状态的服务类(Service、DAO)
- 工具类、配置类
- 线程安全的组件
2. Prototype(原型作用域)
最容易踩坑的作用域。
特征
- 每次通过
getBean()获取时,都会创建一个全新的实例。 - Spring 容器只负责创建和初始化,不负责销毁。
生命周期
- 创建:每次获取时创建(包括初始化流程)。
- 初始化 :完整走完 1~6 步 (和 singleton 一样,
@PostConstruct每次都会执行)。 - 销毁 :容器不会调用任何销毁方法 (
@PreDestroy、DisposableBean统统不执行)。 - 结论:Spring 只负责创建和初始化,资源释放需要开发者手动管理。
为什么这样设计?
因为原型 Bean 可能被任意多个地方持有,Spring 无法知道什么时候该销毁它。
代码示例
java
@Component
@Scope("prototype")
public class PrototypeBean implements DisposableBean {
private int counter = 0;
public PrototypeBean() {
System.out.println("PrototypeBean 实例化,counter=" + (++counter));
}
@PostConstruct
public void init() {
System.out.println("@PostConstruct 执行");
}
@PreDestroy
public void preDestroy() {
System.out.println("@PreDestroy 执行"); // 永远不会打印!
}
@Override
public void destroy() {
System.out.println("DisposableBean.destroy 执行"); // 永远不会打印!
}
}
如何手动销毁原型 Bean?
java
@Component
public class PrototypeManager {
@Autowired
private ApplicationContext context;
private final List<DisposableBean> prototypes = new ArrayList<>();
public <T> T getPrototype(Class<T> clazz) {
T bean = context.getBean(clazz);
if (bean instanceof DisposableBean) {
prototypes.add((DisposableBean) bean);
}
return bean;
}
@PreDestroy
public void destroyPrototypes() throws Exception {
for (DisposableBean bean : prototypes) {
bean.destroy();
}
}
}
适用场景
- 有状态的 Bean(每个调用者需要独立的状态)
- 非线程安全的对象
- 需要频繁创建临时对象的场景
3. Request(请求作用域)【Web 环境】
特征
- 每个 HTTP 请求会创建一个全新的 Bean 实例。
- 同一个请求内,多次获取返回同一个对象。
- 不同请求之间互不干扰。
生命周期
- 创建:第一次在当前请求中访问该 Bean 时创建。
- 初始化:完整走完 1~6 步。
- 销毁:HTTP 请求结束时,由 Web 容器销毁。
- 特殊要求 :必须使用 代理模式。
为什么需要代理?
因为单例 Bean(如 Controller)在启动时就创建了,此时还没有 HTTP 请求。Spring 会注入一个代理对象,代理对象在每次调用时,再去当前请求中查找真正的实例。
代码示例
java
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {
private String requestId = UUID.randomUUID().toString();
private long timestamp = System.currentTimeMillis();
public String getRequestId() {
return requestId;
}
public long getTimestamp() {
return timestamp;
}
}
@RestController
public class TestController {
@Autowired
private RequestBean requestBean; // 注入的是代理对象
@GetMapping("/test")
public String test() {
return "Request ID: " + requestBean.getRequestId() +
", Timestamp: " + requestBean.getTimestamp();
}
}
多次请求会看到不同的 requestId 和时间戳。
适用场景
- 请求级别的日志追踪(每个请求生成一个 traceId)
- 请求级别的临时数据缓存
- 分页参数、当前用户信息等
4. Session(会话作用域)【Web 环境】
特征
- 每个 HTTP Session 会创建一个 Bean 实例。
- 同一个用户的多个请求(同一 Session)共享同一个实例。
- 不同用户之间互不干扰。
生命周期
- 创建:第一次在当前 Session 中访问该 Bean 时创建。
- 初始化:完整走完 1~6 步。
- 销毁:Session 过期或主动销毁时,由 Web 容器销毁。
- 特殊要求 :必须使用 代理模式(同 request 作用域)。
看到这里感觉这个玩意儿好像能当ThreadLocal去使用?但是其实是不太能行的
| 维度 | Request 作用域 | ThreadLocal |
|---|---|---|
| 生命周期 | HTTP 请求开始→结束 | 线程开始→结束(需手动清理) |
| 适用环境 | 仅限 Web 环境 | 任何 Java 环境 |
| 跨层访问 | 需通过注入或 AOP | 同一线程内任意位置直接访问 |
| 实现原理 | Spring 容器管理的代理对象 | JDK 原生线程局部变量 |
| 资源清理 | 自动(请求结束) | 手动(finally 中 remove) |
| 性能开销 | 代理 + 容器查找 | 极低(ThreadLocalMap) |
| 异步支持 | 差(子线程拿不到) | 差(需 InheritableThreadLocal) |
Request作用域不能完全替代ThreadLocal:Request作用域仅适用于Web环境且生命周期绑定HTTP请求,而ThreadLocal可用于任何Java环境(定时任务、MQ、RPC等),并且性能更高、生命周期控制更灵活。两者是互补关系------Web层传递请求级数据优先用Request作用域(自动清理更优雅),框架底层、非Web环境或性能敏感场景则必须用ThreadLocal。
代码示例
java
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionBean {
private String sessionId;
private String username;
private List<String> cart = new ArrayList<>();
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public void setUsername(String username) {
this.username = username;
}
public void addToCart(String item) {
cart.add(item);
}
public List<String> getCart() {
return cart;
}
}
@RestController
public class CartController {
@Autowired
private SessionBean sessionBean;
@PostMapping("/cart/add")
public String addToCart(@RequestParam String item) {
sessionBean.addToCart(item);
return "Added: " + item;
}
@GetMapping("/cart")
public List<String> getCart() {
return sessionBean.getCart();
}
}
同一个用户的购物车数据会持久化在整个 Session 生命周期中。
适用场景
- 用户购物车
- 用户登录信息(替代方案:直接存 Session,但用 Bean 更优雅)
- 用户偏好设置
5. Application(应用作用域)【Web 环境】
特征
- 整个 ServletContext 级别只有一个实例。
- 所有用户、所有请求共享同一个对象。
- 类似于
singleton,但生命周期与 ServletContext 绑定。
与 Singleton 的区别
| 特性 | singleton | application |
|---|---|---|
| 生命周期绑定 | Spring 容器 | ServletContext |
| 一个应用多容器 | 每个容器有自己的实例 | 所有容器共享同一个实例 |
| 使用场景 | 普通 Spring Boot 应用 | 传统 Web 应用(多个 DispatcherServlet) |
在绝大多数 Spring Boot 应用中(只有一个容器),singleton 和 application 行为几乎一样。
生命周期
- 创建:ServletContext 初始化时创建。
- 初始化:完整走完 1~6 步。
- 销毁:应用关闭时,由 Web 容器销毁。
- 特殊要求 :必须使用 代理模式(因为可能被非 Web 组件引用)。
代码示例
java
@Component
@Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationBean {
private long startTime = System.currentTimeMillis();
private long requestCount = 0;
public synchronized void incrementCount() {
requestCount++;
}
public long getRequestCount() {
return requestCount;
}
public long getUptime() {
return System.currentTimeMillis() - startTime;
}
}
@RestController
public class StatsController {
@Autowired
private ApplicationBean appBean;
@GetMapping("/stats")
public String getStats() {
appBean.incrementCount();
return "Uptime: " + appBean.getUptime() + "ms, Requests: " + appBean.getRequestCount();
}
}
所有用户访问 /stats 都会看到累计的请求数。
适用场景
- 全局计数器
- 应用级别的配置缓存
- 共享的全局状态(注意线程安全)
6. WebSocket(WebSocket 作用域)【Web 环境】
特征
- 每个 WebSocket 会话(连接)会创建一个 Bean 实例。
- 同一个 WebSocket 连接内的多次消息交互共享同一个实例。
- 不同连接之间互不干扰。
生命周期
- 创建:WebSocket 连接建立时创建。
- 初始化:完整走完 1~6 步。
- 销毁:WebSocket 连接关闭时,由 WebSocket 容器销毁。
- 特殊要求 :必须使用 代理模式。
前置条件:启用 WebSocket
java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
代码示例
java
@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketSessionBean {
private String sessionId;
private String username;
private List<String> messages = new ArrayList<>();
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public void setUsername(String username) {
this.username = username;
}
public void addMessage(String msg) {
messages.add(msg);
}
public List<String> getMessages() {
return messages;
}
}
@Controller
public class WebSocketController {
@Autowired
private WebSocketSessionBean sessionBean;
@MessageMapping("/chat")
@SendTo("/topic/messages")
public String handleMessage(String message) {
sessionBean.addMessage(message);
return "[" + sessionBean.getSessionId() + "] " + message;
}
}
每个 WebSocket 连接都有自己独立的会话状态。
适用场景
- 聊天室中的用户临时状态
- WebSocket 连接的会话管理
- 实时协作应用中的临时数据
混合作用域注入的注意事项
问题 1:Singleton 注入 Prototype
java
@Component
@Scope("singleton")
public class SingletonBean {
@Autowired
private PrototypeBean prototypeBean; // 注入后不会更新!
public void show() {
System.out.println(prototypeBean.hashCode()); // 永远相同
}
}
解决方案 :使用 ObjectFactory、@Lookup 或 ApplicationContext
java
@Component
public class SingletonBean {
@Autowired
private ObjectFactory<PrototypeBean> prototypeFactory;
public void show() {
PrototypeBean bean = prototypeFactory.getObject();
System.out.println(bean.hashCode()); // 每次不同
}
}
问题 2:Prototype 注入 Singleton
java
@Component
@Scope("prototype")
public class PrototypeBean {
@Autowired
private SingletonBean singletonBean; // 正常工作,注入同一个单例
}
这是没问题的,每次创建 prototype Bean 时都会注入同一个 singleton 实例。
问题 3:Singleton 注入 Request/Session
java
@Component // 默认 singleton
public class MyService {
@Autowired
private RequestBean requestBean; // 需要代理!
}
必须加代理,否则启动时会报错。正确的 Request Bean 定义:
java
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean { ... }
问题 4:原型 Bean 中的资源释放
java
@Component
@Scope("prototype")
public class PrototypeWithResource {
private Connection connection;
public PrototypeWithResource() {
connection = DriverManager.getConnection(...); // 模拟资源
}
@PreDestroy
public void cleanup() {
connection.close(); // 永远不会被调用!
}
}
解决方案:手动管理
java
@Component
public class PrototypeManager {
private final List<AutoCloseable> resources = new ArrayList<>();
public <T> T getPrototype(Class<T> clazz) {
T bean = context.getBean(clazz);
if (bean instanceof AutoCloseable) {
resources.add((AutoCloseable) bean);
}
return bean;
}
@PreDestroy
public void cleanup() {
resources.forEach(resource -> {
try { resource.close(); } catch (Exception e) { /* log */ }
});
}
}