跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

前面的 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 应用可以并发处理多个请求,同一个服务实例因此可能同时被多个线程调用。

适合保存到单例字段中的是固定依赖,而不是某一次请求的数据:

src/main/java/cn/usehook/spring/health/HealthService.java
01
@Service
02
public class HealthService {
03
04
private final HealthProperties properties;
05
06
public HealthService(HealthProperties properties) {
07
this.properties = properties;
08
}
09
10
public HealthStatus check(String service) {
11
String normalizedService = service.strip().toLowerCase(Locale.ROOT);
12
return new HealthStatus(properties.status(), normalizedService);
13
}
14
}

properties 是构造后不再替换的依赖,normalizedService 是每次调用独有的局部变量。当前用户、请求参数、上一次检查结果等数据不能放进单例服务的普通可变字段,否则多个请求会互相覆盖。

无状态并不意味着对象没有字段,而是它不保存随单次调用变化的共享业务状态。连接池、Repository 等 Bean 可以持有线程安全的基础设施依赖,但不能把某条连接或某次事务保存在实例字段中跨请求使用。

3. Prototype 与请求作用域

使用 @Scope 可以声明 prototype:

src/main/java/cn/usehook/spring/health/HealthProbe.java
01
package cn.usehook.spring.health;
02
03
import java.util.UUID;
04
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
05
import org.springframework.context.annotation.Scope;
06
import org.springframework.stereotype.Component;
07
08
@Component
09
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
10
public class HealthProbe {
11
12
private final UUID id = UUID.randomUUID();
13
14
public UUID getId() {
15
return id;
16
}
17
}

每次执行 context.getBean(HealthProbe.class) 都会创建一个实例。prototype 的含义是「每次向容器请求」,不是每次调用 Bean 方法,也不是每个 HTTP 请求。

真正需要每个请求一个实例时,使用 @RequestScope:

src/main/java/cn/usehook/spring/web/RequestTrace.java
01
package cn.usehook.spring.web;
02
03
import java.util.UUID;
04
import org.springframework.stereotype.Component;
05
import org.springframework.web.context.annotation.RequestScope;
06
07
@Component
08
@RequestScope
09
public class RequestTrace {
10
11
private final UUID requestId = UUID.randomUUID();
12
13
public UUID getRequestId() {
14
return requestId;
15
}
16
}

同一个 HTTP 请求中多次使用 RequestTrace 会得到同一个标识,下一个请求则使用新实例。请求对象只在有效 Web 请求上下文中存在,不能在没有请求的普通后台线程中直接取得。

4. 跨作用域依赖

如果单例服务直接通过构造器注入 prototype,依赖只在创建单例时解析一次:

src/main/java/cn/usehook/spring/health/HealthService.java
01
@Service
02
public class HealthService {
03
04
private final HealthProbe probe;
05
06
public HealthService(HealthProbe probe) {
07
this.probe = probe;
08
}
09
10
public UUID currentProbeId() {
11
return probe.getId();
12
}
13
}

多次调用 currentProbeId() 仍然得到同一个标识,因为字段中保存的是创建服务时取得的那一个探针。需要每次业务操作重新取得实例时,显式注入提供者:

src/main/java/cn/usehook/spring/health/HealthService.java
01
@Service
02
public class HealthService {
03
04
private final ObjectProvider<HealthProbe> probes;
05
06
public HealthService(ObjectProvider<HealthProbe> probes) {
07
this.probes = probes;
08
}
09
10
public UUID createProbeId() {
11
return probes.getObject().getId();
12
}
13
}

每次 getObject() 都会向容器请求 prototype Bean。只有业务确实需要有状态新对象时才这样设计,普通无状态服务继续使用单例更清晰。

单例依赖请求级 Bean 时,@RequestScope 默认通过作用域代理解决生命周期差异。注入服务的是代理,调用方法时代理再找到当前请求的真实 RequestTrace。因此单例不会固定保存第一次请求的实例。

5. Bean 生命周期

默认单例由容器管理完整生命周期:

bean-lifecycle.txt
1
注册 Bean 定义
2
-> 调用构造方法
3
-> 注入依赖
4
-> BeanPostProcessor 初始化前处理
5
-> 执行初始化回调
6
-> BeanPostProcessor 初始化后处理并可能返回代理
7
-> Bean 对外可用
8
-> 上下文关闭时执行销毁回调

项目组件可以使用 @PostConstruct 和 @PreDestroy:

src/main/java/cn/usehook/spring/health/HealthReporter.java
01
package cn.usehook.spring.health;
02
03
import jakarta.annotation.PostConstruct;
04
import jakarta.annotation.PreDestroy;
05
import org.springframework.stereotype.Component;
06
07
@Component
08
public class HealthReporter {
09
10
@PostConstruct
11
public void initialize() {
12
System.out.println("HealthReporter initialized");
13
}
14
15
@PreDestroy
16
public void destroy() {
17
System.out.println("HealthReporter destroyed");
18
}
19
}

初始化回调在依赖注入完成后、Bean 对外使用前执行,适合校验状态或准备资源。销毁回调在上下文正常关闭时执行;进程被强制终止时不保证有机会运行,所以关键数据不能只在销毁回调中保存。

第三方类型可以在 @Bean 上指定 initMethod 和 destroyMethod,不必让第三方类依赖 Spring 注解。prototype 则只由容器负责创建、注入和初始化;对象交给调用方以后,容器不会继续跟踪,也不会自动执行销毁回调,需要调用方自行清理资源。

6. 创建时机与验证

应用上下文默认在启动阶段创建非懒加载单例,可以尽早发现构造失败、缺少依赖和初始化异常。某个对象创建成本较高且可能完全不用时,可以有针对性地使用 @Lazy:

src/main/java/cn/usehook/spring/health/RemoteHealthClient.java
01
package cn.usehook.spring.health;
02
03
import org.springframework.context.annotation.Lazy;
04
import org.springframework.stereotype.Component;
05
06
@Component
07
@Lazy
08
public class RemoteHealthClient {
09
10
public RemoteHealthClient() {
11
System.out.println("RemoteHealthClient created");
12
}
13
}

如果一个非懒加载单例直接依赖它,容器仍可能为了完成构造器注入而提前创建真实对象。希望延迟到第一次方法调用,可以在注入点使用 @Lazy 让容器先提供代理。懒加载会推迟配置错误暴露时间,不应作为全局性能开关。

可以用上下文验证单例和 prototype 的实例关系:

scope-observation.java
1
HealthService firstService = context.getBean(HealthService.class);
2
HealthService secondService = context.getBean(HealthService.class);
3
4
HealthProbe firstProbe = context.getBean(HealthProbe.class);
5
HealthProbe secondProbe = context.getBean(HealthProbe.class);
6
7
System.out.println(firstService == secondService); // true
8
System.out.println(firstProbe == secondProbe); // false

作用域和生命周期解决的是组件实例怎样存在,不是框架组件为什么出现,也不是方法调用怎样被额外行为包围。下一篇先学习 Spring Boot 自动配置,再通过 AOP 与代理理解事务等横切能力,之后进入 Spring MVC 请求处理流程。