前面的 Spring Bean 章节解释了 Bean 怎样注册和选择,配置绑定章节又创建了可注入的 HealthProperties。还剩两个与实例本身有关的问题:一个 Bean 定义会创建多少个对象,以及这些对象从什么时候存在到什么时候结束。
作用域决定实例的共享范围,生命周期决定实例经历的创建、初始化和销毁阶段。本章先从默认单例开始,再处理 prototype、请求作用域、跨作用域依赖和懒加载。
1. Bean 作用域
常见作用域如下:
| 作用域 | 实例范围 | 适用场景 |
|---|---|---|
singleton | 每个容器、每个 Bean 定义共享一个实例 | 无状态的 Controller、Service、Repository |
prototype | 每次向容器请求时创建新实例 | 需要独立状态且由调用方继续管理的对象 |
request | 每个 HTTP 请求一个实例 | 只属于当前请求的数据 |
session | 每个 HTTP Session 一个实例 | 会话级状态 |
application | 每个 ServletContext 一个实例 | Web 应用共享对象 |
websocket | 每个 WebSocket 会话一个实例 | WebSocket 会话状态 |
Spring 的 singleton 不是 JVM 全局单例。它准确表示同一个 ApplicationContext 中,同一个 Bean 定义共享一个实例;另一个上下文或另一个 Bean 定义可以产生不同对象。
作用域也不改变 Java 类本身。普通代码仍然可以在容器外调用 new HealthService(...),这不会取得容器中的单例 Bean。
2. 单例与并发请求
HealthController 和 HealthService 没有显式声明作用域,默认都是单例。Web 应用可以并发处理多个请求,同一个服务实例因此可能同时被多个线程调用。
适合保存到单例字段中的是固定依赖,而不是某一次请求的数据:
01@Service02public class HealthService {0304private final HealthProperties properties;0506public HealthService(HealthProperties properties) {07this.properties = properties;08}0910public HealthStatus check(String service) {11String normalizedService = service.strip().toLowerCase(Locale.ROOT);12return new HealthStatus(properties.status(), normalizedService);13}14}
properties 是构造后不再替换的依赖,normalizedService 是每次调用独有的局部变量。当前用户、请求参数、上一次检查结果等数据不能放进单例服务的普通可变字段,否则多个请求会互相覆盖。
无状态并不意味着对象没有字段,而是它不保存随单次调用变化的共享业务状态。连接池、Repository 等 Bean 可以持有线程安全的基础设施依赖,但不能把某条连接或某次事务保存在实例字段中跨请求使用。
3. Prototype 与请求作用域
使用 @Scope 可以声明 prototype:
01package cn.usehook.spring.health;0203import java.util.UUID;04import org.springframework.beans.factory.config.ConfigurableBeanFactory;05import org.springframework.context.annotation.Scope;06import org.springframework.stereotype.Component;0708@Component09@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)10public class HealthProbe {1112private final UUID id = UUID.randomUUID();1314public UUID getId() {15return id;16}17}
每次执行 context.getBean(HealthProbe.class) 都会创建一个实例。prototype 的含义是「每次向容器请求」,不是每次调用 Bean 方法,也不是每个 HTTP 请求。
真正需要每个请求一个实例时,使用 @RequestScope:
01package cn.usehook.spring.web;0203import java.util.UUID;04import org.springframework.stereotype.Component;05import org.springframework.web.context.annotation.RequestScope;0607@Component08@RequestScope09public class RequestTrace {1011private final UUID requestId = UUID.randomUUID();1213public UUID getRequestId() {14return requestId;15}16}
同一个 HTTP 请求中多次使用 RequestTrace 会得到同一个标识,下一个请求则使用新实例。请求对象只在有效 Web 请求上下文中存在,不能在没有请求的普通后台线程中直接取得。
4. 跨作用域依赖
如果单例服务直接通过构造器注入 prototype,依赖只在创建单例时解析一次:
01@Service02public class HealthService {0304private final HealthProbe probe;0506public HealthService(HealthProbe probe) {07this.probe = probe;08}0910public UUID currentProbeId() {11return probe.getId();12}13}
多次调用 currentProbeId() 仍然得到同一个标识,因为字段中保存的是创建服务时取得的那一个探针。需要每次业务操作重新取得实例时,显式注入提供者:
01@Service02public class HealthService {0304private final ObjectProvider<HealthProbe> probes;0506public HealthService(ObjectProvider<HealthProbe> probes) {07this.probes = probes;08}0910public UUID createProbeId() {11return probes.getObject().getId();12}13}
每次 getObject() 都会向容器请求 prototype Bean。只有业务确实需要有状态新对象时才这样设计,普通无状态服务继续使用单例更清晰。
单例依赖请求级 Bean 时,@RequestScope 默认通过作用域代理解决生命周期差异。注入服务的是代理,调用方法时代理再找到当前请求的真实 RequestTrace。因此单例不会固定保存第一次请求的实例。
5. Bean 生命周期
默认单例由容器管理完整生命周期:
1注册 Bean 定义2-> 调用构造方法3-> 注入依赖4-> BeanPostProcessor 初始化前处理5-> 执行初始化回调6-> BeanPostProcessor 初始化后处理并可能返回代理7-> Bean 对外可用8-> 上下文关闭时执行销毁回调
项目组件可以使用 @PostConstruct 和 @PreDestroy:
01package cn.usehook.spring.health;0203import jakarta.annotation.PostConstruct;04import jakarta.annotation.PreDestroy;05import org.springframework.stereotype.Component;0607@Component08public class HealthReporter {0910@PostConstruct11public void initialize() {12System.out.println("HealthReporter initialized");13}1415@PreDestroy16public void destroy() {17System.out.println("HealthReporter destroyed");18}19}
初始化回调在依赖注入完成后、Bean 对外使用前执行,适合校验状态或准备资源。销毁回调在上下文正常关闭时执行;进程被强制终止时不保证有机会运行,所以关键数据不能只在销毁回调中保存。
第三方类型可以在 @Bean 上指定 initMethod 和 destroyMethod,不必让第三方类依赖 Spring 注解。prototype 则只由容器负责创建、注入和初始化;对象交给调用方以后,容器不会继续跟踪,也不会自动执行销毁回调,需要调用方自行清理资源。
6. 创建时机与验证
应用上下文默认在启动阶段创建非懒加载单例,可以尽早发现构造失败、缺少依赖和初始化异常。某个对象创建成本较高且可能完全不用时,可以有针对性地使用 @Lazy:
01package cn.usehook.spring.health;0203import org.springframework.context.annotation.Lazy;04import org.springframework.stereotype.Component;0506@Component07@Lazy08public class RemoteHealthClient {0910public RemoteHealthClient() {11System.out.println("RemoteHealthClient created");12}13}
如果一个非懒加载单例直接依赖它,容器仍可能为了完成构造器注入而提前创建真实对象。希望延迟到第一次方法调用,可以在注入点使用 @Lazy 让容器先提供代理。懒加载会推迟配置错误暴露时间,不应作为全局性能开关。
可以用上下文验证单例和 prototype 的实例关系:
1HealthService firstService = context.getBean(HealthService.class);2HealthService secondService = context.getBean(HealthService.class);34HealthProbe firstProbe = context.getBean(HealthProbe.class);5HealthProbe secondProbe = context.getBean(HealthProbe.class);67System.out.println(firstService == secondService); // true8System.out.println(firstProbe == secondProbe); // false
作用域和生命周期解决的是组件实例怎样存在,不是框架组件为什么出现,也不是方法调用怎样被额外行为包围。下一篇先学习 Spring Boot 自动配置,再通过 AOP 与代理理解事务等横切能力,之后进入 Spring MVC 请求处理流程。