CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Bean 作用域与生命周期
这一课你将能做到
用 @Configuration + @Bean 把第三方类注册进容器,并说清它和 @Component 的分工
演示 singleton 与 prototype 两种作用域的行为差异,并解决「单例注入多例」的经典坑
在 Bean 的关键生命周期节点(初始化、销毁)挂上自己的逻辑
为什么要学
上一课所有对象默认「全局一个实例」(singleton),你有没有想过:如果 Bean 里放了个会被并发修改的成员变量会怎样?如果每次请求都需要一个全新对象怎么办?对象初始化完、应用关闭时,连接池、线程池谁来收拾?
人话:作用域回答「一个 Bean 建几份、什么时候建」;生命周期回答「它出生、干活、死亡时我能插手什么」 。连接池启动时预热、缓存启动时加载、应用关闭时优雅释放——全靠生命周期钩子。这也是面试 Spring 部分的必考区。
先建立图像
一个 singleton Bean 的一生:
扫描/解析定义 → 实例化(调构造器)→ 属性注入(依赖塞进来)
→ Aware 回调 → BeanPostProcessor 前置处理
→ 初始化(@PostConstruct → InitializingBean → initMethod)
→ BeanPostProcessor 后置处理(AOP 代理就在这生成!)
→ 放入单例池,供业务使用
→ 容器关闭 → @PreDestroy → destroyMethod
记住两个锚点:属性注入在构造器之后 (所以构造器里读不到注入的字段值);AOP 代理在后置处理器阶段生成 (所以代理能包住你的 Bean)。
作用域 人话 典型场景 singleton(默认) 全容器一个,启动时就建好 Service、Repository、工具类 prototype 每次要的时候现建一个新的 每次需要独立状态的对象 request / session 每个 HTTP 请求 / 会话一个 Web 场景的请求级上下文 application 每个 ServletContext 一个 全局 Web 级缓存
跟着做
步骤 1:用 @Configuration + @Bean 注册一个第三方类
Jackson 的 ObjectMapper 是现成的第三方类,假设我们想自定义它的行为:
package com.example.taskflow.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 声明这是「Bean 定义」的来源
public class AppConfig {
@Bean // 方法返回值注册进容器,Bean 名默认=方法名
public ObjectMapper customObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 遇到未知字段不报错(前后端字段对不齐时很救命)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}
任意 Controller 注入验证:
@GetMapping("/api/om-check")
public String omCheck(ObjectMapper injected) {
return injected.getClass().getSimpleName();
}
分工口诀:自己的类用 @Component 系注解(类上标注),别人的类用 @Bean(方法上注册) ——你改不了第三方类的源码,加不了 @Component。
步骤 2:singleton——默认作用域的行为 package com.example.taskflow.counter;
import org.springframework.stereotype.Component;
@Component
public class VisitCounter {
private int count = 0; // 故意演示成员变量
public int increase() { return ++count; }
}
@RestController
public class CounterController {
private final VisitCounter counter;
public CounterController(VisitCounter counter) {
this.counter = counter;
}
@GetMapping("/api/visit")
public int visit() { return counter.increase(); }
}
反复访问 /api/visit:1、2、3……所有请求共享同一个实例和同一份 count 。这引出两个结论:①想跨请求计数,容器帮你做了(无需 static);②singleton 里放可变状态,多线程并发修改必然出问题(++count 不是原子操作)。
结论写进笔记:singleton Bean 必须无状态;要状态就放方法参数、局部变量,或交给 Redis/数据库。
步骤 3:prototype——每次都要新的 package com.example.taskflow.counter;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype") // 每次获取都新建实例
public class ReportBuilder {
private final StringBuilder sb = new StringBuilder();
public ReportBuilder append(String line) {
sb.append(line).append('\n');
return this;
}
public String build() { return sb.toString(); }
}
@GetMapping("/api/report")
public String report() {
ReportBuilder b1 = applicationContext.getBean(ReportBuilder.class);
ReportBuilder b2 = applicationContext.getBean(ReportBuilder.class);
return "b1==b2 ? " + (b1 == b2); // false:每次 getBean 都是新的
}
(applicationContext 通过构造器注入 ApplicationContext 获得。)prototype 的语义是「容器只负责造,不管后续」:销毁回调对它无效,容器不会追踪 prototype 的死亡 。
步骤 4:单例注入多例的经典坑 直接给 singleton 注入 prototype 会发生什么?
@Service
public class TaskService {
private final ReportBuilder builder; // 只在启动时注入了一次!
public TaskService(ReportBuilder builder) {
this.builder = builder;
}
}
启动注入时创建了一个实例,之后这个 singleton 用到死都是同一个 builder ——prototype 失效了。正确姿势一:用 ObjectProvider 每次现取:
@Service
public class TaskService {
private final ObjectProvider<ReportBuilder> builderProvider;
public TaskService(ObjectProvider<ReportBuilder> builderProvider) {
this.builderProvider = builderProvider;
}
public String export() {
ReportBuilder builder = builderProvider.getObject(); // 每次调用都是新实例
return builder.append("任务导出于 " + java.time.LocalDateTime.now()).build();
}
}
正确姿势二:在 prototype 类上加 @Lookup 抽象方法(了解即可)。记住结论即可:singleton 要用 prototype,必须「按需获取」,不能「一次性注入」 。
步骤 5:生命周期钩子——初始化与销毁 package com.example.taskflow.lifecycle;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class DictCache {
private final Map<String, String> dict = new ConcurrentHashMap<>();
@PostConstruct // 依赖注入完成后执行,只执行一次
public void load() {
System.out.println("[DictCache] 启动预热:加载字典数据");
dict.put("status_0", "待处理");
dict.put("status_1", "进行中");
dict.put("status_2", "已完成");
}
public String label(String code) { return dict.getOrDefault(code, code); }
@PreDestroy // 容器关闭前执行
public void cleanup() {
System.out.println("[DictCache] 应用关闭:释放字典缓存");
dict.clear();
}
}
重启应用观察控制台:启动时先输出「启动预热」,Ctrl+C 关闭时输出「应用关闭」。三个可用钩子按执行顺序:
@PostConstruct // ① 推荐,JSR 标准,注解方法
public void init() { }
// ② 实现 InitializingBean / DisposableBean 接口(侵入性强,少用)
// ③ @Bean(initMethod = "start", destroyMethod = "stop")——第三方类没有注解时的唯一选择
应用优雅停机要生效,可在 yml 里开启 server.shutdown: graceful,让在途请求处理完再销毁 Bean。
步骤 6:@Lazy——延迟初始化 @Component
@Lazy
public class HeavyInitializer {
public HeavyInitializer() {
System.out.println("[HeavyInitializer] 构造了(现在才发生)");
}
}
启动日志里看不到它,直到第一次被使用才创建。用途:加载慢的 Bean(如重量级客户端)不拖慢启动;也用于打破构造器循环依赖。
讲清楚要点
singleton 默认且够用 :Web 应用 99% 的 Bean 都是单例。代价是要遵守「无状态」纪律;有状态需求用方法局部变量、ThreadLocal 或外部存储解决,而不是改作用域。
prototype 的「多例」只在 getBean/getObject 时发生 :注入动作只发生一次。注入 prototype 到 singleton,拿到的是启动那一刻的单个实例。
初始化钩子的正确用法 :@PostConstruct 里做「依赖已就绪」后的准备工作(预热缓存、校验配置、注册回调);此时字段注入已完成,但注意别在这里做依赖外部系统的重活拖慢启动。
销毁钩子只对 singleton 有效 :prototype 对象容器不追踪。连接池、线程池这类需要关闭的资源必须是 singleton。
BeanPostProcessor 是 AOP 的地基 :Spring 在初始化前后留了全局钩子,@Transactional、@Async、@Cacheable 生成的代理全在这里织入。理解它,就理解了「注解魔法」的发生位置。
你可能会懵的地方
singleton Bean 是线程安全的吗?
Bean 本身无所谓安全不安全,取决于你怎么用:无状态(只有 final 依赖和方法内局部变量)就是安全的;有可变成员变量被多请求并发读写就不安全。不要因为「怕线程不安全」改 prototype——那只是把并发问题换成了「每个请求都重建重资源」的性能问题。
@PostConstruct 和构造器的区别?
构造器执行时依赖还没注入;@PostConstruct 在注入完成后执行。需要用到注入依赖的初始化逻辑必须放 @PostConstruct。
request/session 作用域为什么实际项目很少见?
前后端分离架构下,请求级状态通常走参数、ThreadLocal(如拦截器存用户信息)或直接传值;session 作用域在无状态 API 风格里更少。了解即可,别当主力。
@Bean 方法之间互相调用会怎样?
在 @Configuration 类里调用另一个 @Bean 方法,Spring 通过 CGLIB 代理拦截,返回的仍是容器里的同一个单例(这就是 proxyBeanMethods=true 的作用);若 @Configuration(proxyBeanMethods = false) 则是普通方法调用,会 new 新对象。保持默认即可。
Bean 的加载顺序能控制吗?
@DependsOn 显式声明、@Order 控制集合注入顺序、构造器依赖天然形成顺序。尽量不要依赖隐式顺序,依赖关系靠注入表达。
错法对照 错法 问题 正解 singleton 里放可变成员变量 并发修改,计数错乱 无状态化;状态放局部变量或外部存储 singleton 构造器里读 @Value 注入字段 字段还是 null 初始化逻辑放 @PostConstruct 直接注入 prototype 期望每次新的 只注入一次,退化成单例 ObjectProvider.getObject() 按需取 用 prototype 放数据库连接池 池被反复创建,销毁回调失效 资源类一律 singleton 给第三方类强行加 @Component 改不了源码 @Configuration + @Bean 注册
卡点急救
@PostConstruct 方法抛异常导致启动失败 :这是好事——问题被拦在启动阶段。读异常定位预热逻辑的错误(通常是外部依赖不可达或配置缺失),修好再启动;不要 try-catch 吞掉假装没事。
应用关闭时 @PreDestroy 没执行 :确认是用 Ctrl+C / kill(SIGTERM)正常关闭而不是 kill -9(强杀不给机会);容器未正常启动完成也不会执行销毁。
需要 prototype 却报 Bean 是同一个 :检查是否在注入点直接拿的(注入只发生一次),改成 ObjectProvider 或每次 getBean。
两个 Bean 必须有先后顺序 :加 @DependsOn("beanA") 显式声明,或直接用构造器依赖表达。
面试常问
Spring Bean 的生命周期?
实例化 → 属性注入 → Aware 回调(BeanNameAware 等)→ BeanPostProcessor 前置 → 初始化(@PostConstruct → InitializingBean.afterPropertiesSet → initMethod)→ BeanPostProcessor 后置(AOP 代理在此生成)→ 使用 → 容器关闭 → 销毁(@PreDestroy → DisposableBean.destroy → destroyMethod)。
singleton 和 prototype 的区别?各自注意什么?
singleton 全局一个、容器管理完整生命周期、必须无状态;prototype 每次获取新建、容器只管创建不负责销毁、与 singleton 组合注入时会退化为单例,需 ObjectProvider 按需获取。
单例 Bean 依赖原型 Bean 怎么处理?
ObjectProvider / @Lookup 方法 / Provider.getBean,实现「每次使用都是新实例」;同时评估是否真需要 prototype,多数场景可用无状态设计替代。
Bean 作用域有哪些?request 作用域怎么工作?
singleton、prototype、request、session、application、websocket。request 依赖 RequestContextHolder 的 ThreadLocal 请求上下文,作用域代理在每次请求时路由到当前请求的实例——非 Web 环境使用会报错。
自测三问
Bean 生命周期从实例化到销毁,中间至少说出 5 个关键节点?
为什么构造器里读不到注入的字段?初始化逻辑该放哪?
singleton Service 里放一个 private int count 会有什么后果?正确做法是什么?
小结一句话 作用域决定 Bean「建几份」(默认单例、须无状态),生命周期钩子让你在「出生与死亡」时插手——连接池预热、缓存加载、优雅释放全靠它们。
延伸