真实击剑
93.36 MB · 2025-11-22
刚接触 Spring 那会,我总觉得 bean 的生命周期是块 “看着懂、用着懵” 的软骨头 —— 流程图背得滚瓜烂熟,一到项目里就踩坑:比如 @PreDestroy 写了没执行,初始化方法里拿不到注入的属性,甚至线上因为 bean 创建顺序导致空指针。直到这八年里调过 N 次类似的 bug、自定义过 BeanPostProcessor、排查过循环依赖引发的初始化异常,才真正把这事儿嚼透:Spring 的 bean 生命周期,本质是 “从创建到销毁” 的标准化流程,每一步都藏着 “解耦扩展” 和 “避坑指南” 。
今天不聊干巴巴的理论,就从实战角度拆解 bean 的生命周期 —— 每个阶段讲清楚 “做什么”“实际坑点”“怎么用”,再补上我踩过的真实案例,帮你少走我当年的弯路。
在没 Spring 之前,我们写 Java 是new Object()直接创建对象,初始化、赋值、销毁全靠自己写代码。但项目大了就乱了:比如 100 个 bean 都要初始化数据库连接,总不能每个 bean 里都写一遍吧?某个 bean 销毁时要释放资源,万一忘了调用怎么办?
Spring 的 bean 生命周期,本质就是把 “对象创建 - 初始化 - 使用 - 销毁” 的过程标准化,并且留出扩展点—— 你不用关心 bean 怎么被创建,只需要告诉 Spring:“我要在初始化时做 XX”“销毁时要清 XX 资源”,剩下的交给框架。
简单说,生命周期的核心价值是:解耦创建逻辑、统一扩展入口、避免资源泄漏。
Spring bean 的生命周期,从 “被 Spring 容器管理” 到 “被销毁”,可以分为 5 个核心阶段。我画了张简化流程图(脑子里的):实例化(new对象)→ 属性注入(给字段赋值)→ 初始化(执行自定义逻辑)→ 使用(业务调用)→ 销毁(释放资源)
每个阶段都有细节和坑,咱们逐个拆:
做什么:Spring 根据 bean 的定义(比如 @Component、@Bean),调用类的构造器创建对象 —— 这一步相当于你自己写new UserService(),但由 Spring 帮你执行。
八年开发实战点:
public UserService(OrderService orderService)),Spring 会先去创建依赖的OrderService,再创建UserService。这里要注意:构造器里别写复杂逻辑!我早年踩过坑:在构造器里调用orderService.query(),但此时OrderService可能还没完成初始化,直接空指针。@Autowired标在构造器上,要么确保只有一个有参构造(Spring 会自动用)。别同时给多个构造器加@Autowired,否则 Spring 会懵,直接抛异常。做什么:实例化后,Spring 会给 bean 的属性赋值 —— 比如给加了@Autowired、@Resource的字段赋值,或者调用 setter 方法注入依赖。
八年开发实战点:
@Autowired vs @Resource:别再搞混了!@Autowired是 Spring 注解,按 “类型” 注入;@Resource是 JSR 规范注解,按 “名称” 注入(找不到再按类型)。实战中,如果一个接口有多个实现类(比如PaymentService有AlipayService和WechatPayService),用@Resource(name = "alipayService")比@Autowired + @Qualifier更简洁。
注入的 “时机坑”:属性注入是在实例化之后、初始化之前。所以千万别在构造器里用注入的属性!比如:
@Service
public class UserService {
@Autowired
private OrderService orderService;
// 错误:此时orderService还没注入,是null
public UserService() {
orderService.query();
}
}
解决办法:要么把依赖放进构造器注入,要么把逻辑移到初始化方法里。
字段注入的 “测试坑”:很多人喜欢用字段注入(方便),但写单元测试时会麻烦 —— 因为字段是 private 的,没法直接 mock。八年经验是:核心服务用构造器注入(利于测试),工具类用字段注入(省事),平衡效率和可测试性。
实例化 + 属性注入后,bean 还不能直接用 —— 比如要初始化数据库连接、加载配置文件、初始化缓存。Spring 把这个阶段拆成 3 步,还留出了扩展点,是整个生命周期的核心。
流程顺序(重点记!):BeanPostProcessor前置处理 → 自定义初始化逻辑 → BeanPostProcessor后置处理
做什么:Spring 会遍历所有BeanPostProcessor,调用postProcessBeforeInitialization方法 —— 这是个 “全局钩子”,可以对所有 bean 做统一处理。
实战场景:
@Loggable注解的 bean,在初始化前自动生成日志代理。就是通过自定义BeanPostProcessor,在前置处理里判断 bean 是否有注解,有就生成代理对象。BeanPostProcessor是 “全局生效” 的,如果你只想要某个 bean 生效,记得在方法里加判断(比如if (bean instanceof UserService)),别影响其他 bean。这是我们最常用的部分,Spring 提供了 3 种方式,优先级一定要记清(踩过坑的都懂):
@PostConstruct(JSR 规范注解)InitializingBean接口(Spring 原生接口)init-method(XML 或 @Bean 的属性)八年开发经验:
@PostConstruct:它是 JSR 规范,不依赖 Spring,后续换成其他框架(比如 Jakarta EE)也不用改代码。而InitializingBean是接口,会让你的 bean 和 Spring 耦合,不推荐。@PostConstruct里调用远程接口(万一网络不通,bean 创建失败,整个容器启动不了)。如果有复杂逻辑,建议异步执行(比如用@Async)。@Lazy(延迟注入),而@PostConstruct执行时,延迟注入的 bean 还没创建。解决办法:要么去掉@Lazy,要么在使用时再初始化。做什么:调用postProcessAfterInitialization方法 —— 这是 Spring 实现 AOP 的核心步骤!比如你给 bean 加了@Transactional,Spring 会在这里生成代理对象,替换原来的 bean。
实战注意:
代理对象的 “坑”:如果你的 bean 被 AOP 代理了,那么this关键字调用的方法,不会触发切面(因为this是原始对象,不是代理对象)。比如:
@Service
public class UserService {
@Transactional
public void addUser() {
// 错误:this调用不会触发事务
this.updateUser();
}
@Transactional
public void updateUser() {}
}
解决办法:要么用ApplicationContext获取代理对象,要么用@Autowired自己注入自己(虽然看起来怪,但实战中常用)。
BeanPostProcessor的执行顺序:如果有多个BeanPostProcessor,用@Order注解指定顺序(数字越小越先执行)。比如 Spring 自带的AutowiredAnnotationBeanPostProcessor(处理@Autowired)和你自定义的,要确保顺序正确,否则会导致注入失败。
初始化完成后,bean 就可以被业务代码使用了(比如@Autowired注入后调用方法)。但这里有个容易被忽略的点:bean 的作用域会影响生命周期。
八年开发常用的 2 种作用域:
@Lazy),整个容器里只有一个实例,销毁时随容器关闭。getBean(或注入)时创建新实例,Spring 不管理多例 bean 的销毁!多例的 “销毁坑” :我曾在多例 bean 里写了@PreDestroy,想在销毁时释放数据库连接,结果发现完全不执行。后来才明白:多例 bean 创建后,Spring 就不管了,销毁得自己处理(比如用BeanFactory手动管理,或在业务代码里调用销毁方法)。所以实战中,除非明确需要多例(比如每个请求需要独立状态),否则尽量用单例(性能好,Spring 能管理生命周期)。
当容器关闭时(比如 Web 项目停 Tomcat、命令行项目调用ApplicationContext.close()),单例 bean 会进入销毁阶段。流程顺序和初始化对应:@PreDestroy(JSR 规范) → DisposableBean接口 → destroy-method(XML 或 @Bean 属性)
实战避坑点:
ClassPathXmlApplicationContext或AnnotationConfigApplicationContext,一定要在最后调用close()方法,否则@PreDestroy不执行,资源(比如数据库连接池、线程池)会泄漏。@PreDestroy里抛了未捕获异常,会导致其他 bean 的销毁逻辑被中断。所以销毁时要捕获异常,或者用try-finally确保资源释放。@PreDestroy里调用connection.close()时抛了 NullPointerException(连接对象为 null),没处理异常,导致后续的连接释放逻辑没执行。解决办法:加 null 判断和异常捕获,确保close()方法一定被调用。讲完生命周期的细节,再给大家分享 3 个我实战中总结的原则,帮你少走弯路:
比如用@PostConstruct/@PreDestroy代替InitializingBean/DisposableBean,用@Autowired/@Resource代替 XML 配置。这样你的代码不仅能在 Spring 里跑,换成其他框架(比如 Quarkus)也不用大改,灵活性更高。
initCache(),在第一次调用时初始化)。比如自定义BeanPostProcessor、BeanFactoryPostProcessor时,先搞懂它们的执行时机,别随便改 bean 的状态。我早年曾自定义BeanPostProcessor,在前置处理里修改了 bean 的属性,导致后续注入的依赖被覆盖,排查了半天才找到原因。记住:Spring 的扩展点很强大,但也很容易 “踩雷”,理解原理再用。
现在再看 Spring bean 的生命周期,我已经不把它当 “流程图” 了 —— 而是把它看作 Spring 的 “设计思路”:通过标准化流程,让 bean 的创建、初始化、销毁都 “可控”,同时留出扩展点,让我们能按需定制。
其实不光是 bean 的生命周期,Spring 的很多特性(比如 AOP、事务管理),本质都是在 “解耦” 和 “可控” 之间找平衡。理解了这一点,不管是排查 bug,还是自定义扩展,都能更得心应手。