ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

详解Spring Bean生命周期:实例化、属性填充、初始化、销毁与扩展点

详解Spring Bean生命周期:实例化、属性填充、初始化、销毁与扩展点 Spring Bean 的生命周期这个话题在 Spring 面试里几乎是必问的但很多人背完八股文到了真正排查线上问题时还是一脸懵。我印象特别深的一次是帮同事查一个“定时任务启动后偶尔不执行”的问题最后发现是 Bean 在初始化阶段抛了异常但日志被自定义的BeanPostProcessor吞掉了一部分导致后面所有依赖它的 Bean 全部初始化失败。从那天起我就发现光会背“实例化→属性填充→初始化→销毁”这四步根本不够你得真正理解容器在每一步做了什么、回调点在哪、哪些坑会导致生命周期提前结束或回调失效。这篇文章我会从 Spring Bean 生命周期的完整链条讲起把实例化、属性填充、初始化、销毁这几个核心阶段逐个拆开再把BeanPostProcessor、InitializingBean、PostConstruct、Aware这些实际开发里最常用的扩展点串起来。适合刚学 Spring 的初学者建立整体认知也适合写过一段时间 Spring 但没认真追踪过执行顺序、或者遇到“回调不生效”“生命周期和你以为的不一样”这类问题的同学。我会给出能直接跑的示例代码和完整的日志顺序也会把我在真实项目里踩过的坑一并写出来。1. 理清基础先搞清楚 Spring Bean 生命周期到底解决什么问题1.1 什么是 Bean 生命周期要理解生命周期最好先对比一下“普通 Java 对象”和“Spring Bean”的根本差异。你用new创建一个对象从构造函数执行完毕那刻起这个对象就“活着”了你什么时候销毁它完全由你说了算。但 Spring Bean 不一样它是由 IoC 容器创建、组装、初始化并管理的创建之后还要经历属性赋值、各种回调、代理增强最后在容器关闭时统一销毁。容器替你包办了从“出生”到“死亡”的全过程这个过程就是 Bean 生命周期。所以面试的时候如果你直接把生命周期答成“实例化、属性填充、初始化、销毁”四个词其实是不完整的。完整的理解应该是Spring 在管理每个 Bean 时会按照一套固定的流程去执行这套流程里嵌入了大量回调接口和扩展点你可以通过实现这些接口去干预 Bean 的创建过程从而实现自定义逻辑。这种设计解决了什么问题呢举一个最常见的例子你有一个数据库连接池 Bean希望在应用启动后自动检查一下数据库连通性但又不想把这段逻辑写在业务代码里也不想在构造函数里做因为构造函数执行时属性还没注入连接池配置可能是空的。这时候你只需要让这个 Bean 实现InitializingBean在afterPropertiesSet()里写检查逻辑就行Spring 会保证在属性填充完成后调用它。1.2 生命周期和依赖注入的关系依赖注入和生命周期是两件相互关联但不能混为一谈的事。依赖注入解决的是“Bean 需要什么”的问题生命周期解决的是“Bean 什么时候被创建、什么时候做好使用准备、什么时候销毁”的问题。这里我要提一下最近大家讨论较多的“有效生命周期”这个概念。一个 Bean 从创建到销毁的全流程里真正能对外提供服务、能被其他对象正常使用的阶段是从“初始化完成”到“销毁开始前”这一段。前面的实例化、属性填充只算准备工作后面的销毁阶段 Bean 已经开始释放资源状态不再可靠。理解这个“有效生命周期”特别重要因为很多开发者在PostConstruct里启动了线程、在初始化方法里缓存了大量数据一旦这些准备工作没做完Bean 虽然已经被容器创建出来了但它实际是不可用的。我之前排查过一个案例某个 Bean 初始化时需要读取远程配置初始化耗时超过 10 秒但定时任务在 Spring 容器还没完全启动时就触发了结果拿到的是一个属性全是 null 的半成品 Bean。这就是没有搞清楚“创建完成”和“可以安全使用”之间的区别。1.3 Spring 为什么这么设计如果只从使用者的角度看你可能会觉得 Spring 搞这么一大套回调机制太复杂了。但从框架设计角度看这套机制是必须的Spring 不可能预知每个业务 Bean 初始化时需要做什么它只能定义好一套标准流程然后留出扩展点让使用者在合适的时机插入自己的逻辑。这就像一家酒店客人的入住流程是固定的登记、领房卡、进房间但每个客人入住后想干什么睡觉、开会、游泳酒店管不着酒店只需要规定好“入住后你随时可以去游泳退房前必须归还房卡”。Spring Bean 生命周期也是同样的道理容器把“何时做什么”规定好具体做什么由开发者决定。所以学习这个主题重点不是背下来有哪些接口、哪个先执行而是建立一张“时间线地图”Bean 从无到有经历了哪些节点每个节点 Spring 提供了哪些回调点你可以在回调点里做哪些事、不能做哪些事。有了这张地图你读 Spring 源码、排查问题、设计自己的组件时都会轻松很多。2. 完整的阶段拆解从定义到销毁每个环节在做什么2.1 六个核心阶段速览网上流传的四阶段说法太粗略了我习惯把生命周期拆得更细致一些方便对照源码和日志。完整来看应该包含以下阶段顺序不能乱扫描与注册Spring 扫描到 Bean 定义生成BeanDefinition并注册到容器。这一步在 Bean 实例化之前完成。实例化通过构造函数或工厂方法创建原始对象。属性填充依赖注入把Autowired、Resource、Value等注解标注的依赖注入进去。初始化前置回调执行各种BeanPostProcessor的postProcessBeforeInitialization方法。初始化执行PostConstruct、InitializingBean.afterPropertiesSet()、自定义init-method。初始化后置回调执行BeanPostProcessor.postProcessAfterInitializationAOP 代理通常在这里生成。Bean 就绪Bean 进入“有效生命周期”可以被注入到其他对象或通过容器获取。销毁容器关闭时执行PreDestroy、DisposableBean.destroy()、自定义destroy-method。你可能注意到了第 2、3 阶段之间其实还有一些扩展点比如BeanNameAware、BeanFactoryAware、ApplicationContextAware这些回调它们严格来说发生在属性填充之后、初始化之前。我后面会专门用一章讲这块因为它们太容易被忽略了。2.2 实例化和属性填充的细节实例化阶段是 Bean 真正的“创建”时刻。Spring 会使用InstantiationStrategy来创建实例默认情况下用的是无参构造函数反射创建。如果你定义了有参构造函数Spring 会根据参数类型去容器里找对应的依赖这其实就是构造器注入的原理。这个阶段有一个关键点Spring 为了避免循环依赖在单例 Bean 的实例化后、属性填充前会提前把“早期引用”暴露到一个三级缓存中这就是大家常说的“三级缓存解决循环依赖”。我在这里不想展开讲循环依赖但它和生命周期是强相关的如果你的两个 Bean 互相依赖Spring 会在 A 实例化后、属性填充前就把 A 的半成品对象缓存起来B 创建时就能拿到这个半成品注入进去等 A 继续完成属性填充和初始化整个流程才结束。这也是为什么基于构造器的循环依赖无法解决因为构造器执行时 Bean 还没被实例化出来没有“提前引用”可以暴露。属性填充阶段做的事情非常多。Spring 会先处理Autowired、Resource、Value等注解注入再处理 XML 中配置的property标签。这里有个细节很多人没注意属性填充发生在初始化之前所以在PostConstruct方法里你可以放心使用注入的成员变量但在构造函数里不行。我见过有同事在构造函数里直接使用Value注入的配置项结果拿到的是 null就是因为构造函数执行时属性填充还没开始。2.3 初始化阶段的三种方式初始化是一个比较笼统的说法实际上 Spring 按顺序执行三种初始化逻辑如果同一个 Bean 同时用了三种方式执行顺序是固定的顺序排序穿过 Jsr250 注解 - 接口 - 自定义方法PostConstruct注解标注的方法InitializingBean接口的afterPropertiesSet()方法Bean 定义中指定的init-method或Bean(initMethod )为什么会有三种方式呢这是历史演进的结果。Spring 最早的版本只支持init-method和InitializingBeanInitializingBean是接口实现后代码会和 Spring 耦合init-method通过反射调用指定方法避免了对 Spring 的直接依赖但写法上有点繁琐。PostConstruct是 JDK 自带的 JSR-250 规范注解后来 Spring 也支持了它因为它是注解式声明代码最简洁也符合现代开发习惯。实际开发里我的建议是统一使用PostConstruct理由有三个。第一它是 JSR-250 规范中的标准注解不依赖 Spring 独有 API代码可以移植到其他支持该规范的容器第二注解声明在方法上比实现接口更直观第三Spring Boot 3 和 Spring Framework 6 基于 JDK 17 以后PostConstruct依然是官方文档推荐的方式之一。不过要注意一点如果你在一个类里同时用了PostConstruct和afterPropertiesSet()两个都会执行顺序是先PostConstruct这个行为容易混淆最好避免混合使用。销毁阶段对应的也有三个回调顺序相反PreDestroy注解标注的方法DisposableBean接口的destroy()方法Bean 定义中指定的destroy-method销毁不是垃圾回收。Spring 容器关闭时才能触发销毁回调GC销毁的是没有引用指向的对象不是这里说的生命周期销毁。如果你拿到的 Bean 是原型作用域容器关闭时也不会销毁这个 Bean。3. 生命周期中最容易忽略的扩展点BeanPostProcessor 与 Aware 系列3.1 BeanPostProcessor隐藏在容器底部的钩子很多业务开发做了两三年可能都没直接实现过BeanPostProcessor但它恰恰是 Spring 整个生命周期里最强大的扩展点。它的定义很简单两个方法postProcessBeforeInitialization和postProcessAfterInitialization。这两个方法在生命周期里的位置我已经在前面列出来了。关键在于postProcessAfterInitialization返回的对象会被 Spring 当作最终的 Bean 放进容器。这句话意味着什么意味着这个方法里你可以直接替换掉原来的 Bean 对象甚至可以返回一个动态代理对象。Spring AOP 就是这么实现的AbstractAutoProxyCreator在postProcessAfterInitialization里判断 Bean 是否需要被代理如果需要就返回一个代理对象而不是原始对象。我画过一张非常直观的对比图帮团队里新人理解容器中最终拿到的 Bean 和原型 Bean 的区别BeanPostProcessor就像工厂流水线上一个“改造工位”每个产品出厂前都要经过这里你可以在这个工位上贴标签、加功能甚至整个掉包。产品还是那个型号但实际拿到的可能已经换过了。使用时的注意点有三个。第一BeanPostProcessor本身也是 Bean但它的实例化和普通 Bean 不同容器会优先创建所有BeanPostProcessor所以你在普通 Bean 的初始化回调里看到的BeanPostProcessor已经可用了。第二postProcessBeforeInitialization太早这时PostConstruct还没执行如果你的处理器依赖 Bean 内部状态可能拿到的还是空值。第三如果你在postProcessAfterInitialization里把原来的 Bean 换成了代理对象后续PostConstruct里建立的内部状态可能被代理机制绕过这一点我会在“常见问题”那一章详细说。3.2 Aware 系列回调让 Bean 感知容器Aware翻译过来是“感知”这一系列接口的作用是让一个普通的 Bean 在生命周期早期阶段拿到 Spring 容器的各种资源。常见的有BeanNameAware获取 Bean 在容器中的名字BeanFactoryAware获取BeanFactory实例ApplicationContextAware获取ApplicationContext实例ApplicationEventPublisherAware获取事件发布器EnvironmentAware获取环境变量和配置属性ResourceLoaderAware获取资源加载器这些回调的执行时机大致在属性填充之后、postProcessBeforeInitialization之前。它们解决的问题是有些业务场景需要 Bean 主动感知容器环境比如一个任务调度 Bean 需要动态获取其他 Bean但又不希望把所有依赖都通过注入写死这时实现ApplicationContextAware就能拿到整个容器上下文随时通过getBean()获取所需 Bean。不过我得说句实话现在的 Spring Boot 项目中90% 的 Aware 使用场景都可以用注入替代。EnvironmentAware可以用Value或ConfigurationProperties替代ApplicationContextAware可以用Autowired ApplicationContext替代没必要为了实现而实现。真正需要 Aware 的场景是那些无法通过注入完成的特殊场景比如你要在一个工具类非 Spring Bean里获取容器对象但工具类的生命周期不属于容器管理无法注入这时才需要借助一个静态变量在某个 Bean 的ApplicationContextAware回调中保存容器引用。这是很经典的老项目写法“工具类里用静态 ApplicationContext 拿 Bean”就是这么来的。3.3 回调顺序完整一览这里我把一个单例 Bean 从创建到销毁涉及的全部回调按顺序整理出来你可以把它当作核对表用。假设 Bean 名字叫lifecycleBean构造完成后依次执行序号回调/阶段触发条件1实例化构造函数/工厂方法2属性填充依赖注入完成3setBeanName()实现了BeanNameAware4setBeanFactory()实现了BeanFactoryAware5setApplicationContext()实现了ApplicationContextAware6postProcessBeforeInitialization()所有注册的BeanPostProcessor7PostConstruct方法上有该注解8afterPropertiesSet()实现了InitializingBean9init-methodBeanDefinition 中指定了初始化方法10postProcessAfterInitialization()所有注册的BeanPostProcessorAOP 代理在这11可正常使用有效生命周期12PreDestroy容器关闭方法上有该注解13destroy()实现了DisposableBean14destroy-methodBeanDefinition 中指定了销毁方法这张表是整个生命周期最精华的部分值得你收藏或者贴在自己工位旁边。我发现很多经验丰富的开发记不住第 3、4、5 项的执行顺序因为日常开发和这几个接口打交道少但面试官特别喜欢考这个因为能答出这几项说明你不是死记硬背而是真的看过源码逻辑。4. 实操用代码完整追踪一次 Bean 的生命周期4.1 准备一个“会记日志”的测试 Bean理论讲再多不如跑一次看真实日志。我写了一个测试 Bean把所有回调方法都实现了并在每个方法里打印日志然后运行一个最简单的 Spring 容器观察执行顺序。先定义一个 Bean 类public class LifecycleBean implements InitializingBean, DisposableBean, BeanNameAware, BeanFactoryAware, ApplicationContextAware { private String name; public LifecycleBean() { System.out.println(1. 实例化构造函数执行); } public void setName(String name) { this.name name; System.out.println(2. 属性填充setName 被调用name name); } Override public void setBeanName(String name) { System.out.println(3. BeanNameAwaresetBeanNamebeanName name); } Override public void setBeanFactory(BeanFactory beanFactory) { System.out.println(4. BeanFactoryAwaresetBeanFactory 执行); } Override public void setApplicationContext(ApplicationContext applicationContext) { System.out.println(5. ApplicationContextAwaresetApplicationContext 执行); } PostConstruct public void initWithPostConstruct() { System.out.println(6. PostConstruct初始化方法执行); } Override public void afterPropertiesSet() { System.out.println(7. InitializingBeanafterPropertiesSet 执行); } public void customInitMethod() { System.out.println(8. init-method自定义初始化方法执行); } PreDestroy public void destroyWithPreDestroy() { System.out.println(9. PreDestroy销毁前执行); } Override public void destroy() { System.out.println(10. DisposableBeandestroy 执行); } public void customDestroyMethod() { System.out.println(11. destroy-method自定义销毁方法执行); } public void doWork() { System.out.println(Bean 正常工作name name); } }再写一个BeanPostProcessor打印日志public class LifecyclePostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println(5.5 BeanPostProcessorpostProcessBeforeInitialization 执行); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println(8.5 BeanPostProcessorpostProcessAfterInitialization 执行); } return bean; } }XML 配置里指定自定义初始化和销毁方法bean idlifecycleBean classcom.example.lifecycle.LifecycleBean init-methodcustomInitMethod destroy-methodcustomDestroyMethod property namename valuetest-bean/ /bean bean classcom.example.lifecycle.LifecyclePostProcessor/4.2 运行结果与执行顺序分析我用ClassPathXmlApplicationContext跑了一遍控制台输出如下为了简洁我删掉了 Spring 自身日志1. 实例化构造函数执行 2. 属性填充setName 被调用name test-bean 3. BeanNameAwaresetBeanNamebeanName lifecycleBean 4. BeanFactoryAwaresetBeanFactory 执行 5. ApplicationContextAwaresetApplicationContext 执行 5.5 BeanPostProcessorpostProcessBeforeInitialization 执行 6. PostConstruct初始化方法执行 7. InitializingBeanafterPropertiesSet 执行 8. init-method自定义初始化方法执行 8.5 BeanPostProcessorpostProcessAfterInitialization 执行然后调用doWork()Bean 正常工作name test-bean最后关闭容器输出9. PreDestroy销毁前执行 10. DisposableBeandestroy 执行 11. destroy-method自定义销毁方法执行注意一下我特意把BeanPostProcessor的两行日志编号为 5.5 和 8.5因为它夹在两个大阶段之间。这是很多人容易记混的地方postProcessBeforeInitialization虽然名字带 Initialization但它执行时PostConstruct还没跑postProcessAfterInitialization执行时所有初始化逻辑已经跑完了。如果使用AnnotationConfigApplicationContextBean注解顺序完全一致。Bean注解里的initMethod和destroyMethod参数对应 XML 的init-method和destroy-method。4.3 原型作用域的差异上面的例子用的是默认的单例作用域。如果改成原型作用域生命周期会有两个显著变化第一每次getBean()都会重新执行完整的实例化、属性填充和初始化流程postProcessAfterInitialization也会执行。第二容器关闭时不会回调原型 Bean 的销毁方法。我验证过一次把scopeprototype加在 XML 里关闭容器后只看到单例 Bean 的销毁日志原型 Bean 的PreDestroy和destroy()完全不触发。原因很简单容器只负责创建和管理单例 Bean 的完整生命周期对原型 Bean容器只负责“创建并交付”后续销毁由使用方负责。这就是为什么PreDestroy对原型 Bean 是无效的。如果确实需要销毁原型 Bean只能自己持有引用手动调用销毁逻辑或者使用java.lang.AutoCloseable配合 try-with-resources 自己管理。这里也顺带解释一个常见误区很多人认为 Spring 中所有 Bean 都是单例所以生命周期全局有效。实际上Scope(prototype)的 Bean 每次注入都是不同的实例如果你在单例 Bean 里通过Autowired注入一个原型 Bean实际注入的那个引用是固定的它并不会“每次换新”除非你使用ObjectFactory或Lookup等方式获取新实例。这个和生命周期叠加在一起会产生各种奇怪的问题我后面会再提到。5. 实战中的坑生命周期回调为什么经常“失灵”5.1 代理对象替换导致 PreDestroy 不执行这是我在一个微服务项目里真实遇到的某个服务在启动时要释放一堆线程池资源在PreDestroy里写了关闭逻辑但容器关闭时就是不打日志。排查了半天发现这个 Bean 被 AOP 代理了。因为postProcessAfterInitialization返回的是代理对象Spring 保存的“真实 Bean”是代理对象而代理对象内部没有注册销毁回调时PreDestroy就失效了。这种情况的解决思路有两个。如果确实需要接管销毁逻辑可以改用DisposableBean接口因为 Spring 对实现了该接口的 Bean 会在适配器里调用真正的destroy方法代理场景下更可靠。或者把资源释放逻辑放到一个专门管理生命周期的组件里通过监听ContextClosedEvent事件来做这是最不容易被代理干扰的方案。实现ApplicationListenerContextClosedEvent在onApplicationEvent里完成资源清理能绕过代理机制。5.2 初始化回调里抛出异常Bean 是否还能使用答案是不能。Spring 的生命周期是一条直线任何一环抛出异常整个创建过程就中断。这个异常会向上抛出导致容器启动失败或者getBean()请求失败。特别是在postProcessAfterInitialization里抛异常时受影响的不只是当前 Bean后续所有等待创建的 Bean 都可能连带失败。这个坑最隐蔽的地方在于初始化方法里的异常不一定第一时间出现在日志里。如果你的BeanPostProcessor里存在隐式异常处理比如 catch 之后打了一行 warn 日志看起来启动是成功的但 Bean 实际没有完成初始化。等运行时才发现有些属性是 null、有些依赖没有注入完成定位成本非常高。我的建议是在PostConstruct和afterPropertiesSet()里不要用 try-catch 吞异常让它自然抛出保证启动期就能发现问题。5.3 非 Web 应用不主动销毁 Bean很多人本地测试时发现PreDestroy没触发先检查代码、再检查配置最后发现压根没关闭容器。注意Spring 容器只有在调用close()时才会触发销毁阶段比如ClassPathXmlApplicationContext.close()或AnnotationConfigApplicationContext.close()。在 Web 应用Spring MVC、Spring Boot中容器由 Servlet 容器管理应用关闭时会自动触发销毁逻辑。还有一个非 Web 应用常见的坑如果你直接写一个main函数创建了AnnotationConfigApplicationContext跑完业务代码没有调用close()程序就退出 JVM 了销毁逻辑不会执行。正确做法是把close()放在finally块里或者注册一个 JVM 关闭钩子。Spring 提供了一个方便的方法context.registerShutdownHook()调用后 JVM 退出时会自动执行容器关闭流程PreDestroy和DisposableBean都会照常执行。我写的测试代码里就调用了这个方法所以最后销毁日志完整。5.4 常见问题速查表现象可能原因解法建议PostConstruct里的依赖是 null依赖是构造器注入的参数但构造器尚未执行改用字段注入或 setter 注入在初始化回调里使用PreDestroy不执行Bean 是原型作用域自行管理销毁或改用单例PreDestroy不执行且 Bean 被事务/AOP 代理代理对象包装了原始对象销毁回调没注册实现DisposableBean或监听ContextClosedEvent初始化方法里抛异常但启动成功自定义BeanPostProcessor吞掉了异常严禁在初始化回调里吞异常让异常上抛关闭容器后销毁顺序和预想不同同时使用了PreDestroy、DisposableBean、destroy-method统一用一种方式推荐注解使用Bean时initMethod指向方法但没执行方法签名不正确要求无参且返回 void检查方法访问权限和参数普通类里静态ApplicationContext为 null静态变量赋值时机在setApplicationContext之前把赋值逻辑放到ApplicationContextAware回调中5.5 初始化顺序依赖的坑最后一个要专门提的是“Bean 之间的初始化顺序依赖”。Spring 只保证依赖注入的顺序不保证PostConstruct的绝对先后。举个例子A 和 B 之间没有直接依赖但 A 的PostConstruct里要读 B 初始化时写入的缓存这时如果 B 还没走完初始化流程A 读到的缓存就是空的。解决办法有三个一是显式声明依赖比如让 A 依赖注入 B这样容器会先初始化 B 再初始化 A二是用DependsOn注解强制指定依赖三是不要依赖初始化时机改成懒加载或者通过事件监听解耦。我在实际项目中推荐优先使用DependsOn它语义明确而且不会让 A 和 B 产生不必要的耦合关系。6. 除记忆外如何设计业务代码的生命周期策略6.1 不同场景下该选哪种初始化方式用了这么多年 Spring我总结出了一个比较实用的选择矩阵场景推荐方式理由简单初始化逻辑设置状态、校验配置PostConstruct代码简洁、可读性好需要访问 Spring 容器、动态获取 BeanApplicationContextAwarePostConstructAware 回调能拿到上下文引用需要保证 Bean 创建后一定做某事且不想依赖 Spring APIinit-method解耦 Spring适合工具类复用需要在销毁时释放资源且 Bean 可能被代理监听ContextClosedEvent绕开代理问题可靠异步初始化不想阻塞启动不要写在PostConstruct里初始化回调里做耗时操作会拖慢容器启动特别说明一下“异步初始化”这条。很多开发者希望在启动时异步加载一批数据直接把线程池submit写在PostConstruct里。这个做法不是不行但要注意异步线程执行时机不确定如果后续依赖这批数据的 Bean 在初始化时要读取可能拿到空值。更好的做法是在初始化回调里同步加载关键数据非关键数据放到独立线程并用CountDownLatch控制就绪状态。6.2 初始化方法里到底能不能做耗时操作很多人会问这个问题答案是可以但要分情况。加载本机缓存、建立连接池、读取配置文件这些可以放在初始化里但要注意耗时上限。一个 Bean 的初始化时间会直接累加到容器启动时间上如果你的初始化逻辑牵扯到远程调用、数据库查询启动可能从 1 秒变成 30 秒。而且初始化的异常会导致容器启动失败远程调用在网络抖动时更容易出问题。我的建议是初始化回调里只做“必须在这时做”的操作。比如校验必填配置、创建连接池、加载本地静态数据。需要远程调用时把逻辑改成启动后异步执行或者用ApplicationReadyEvent来触发。在 Spring Boot 中ApplicationReadyEvent是应用完全启动成功后才发布的事件这个时机做远程调用的健壮性远高于PostConstruct。6.3 我在项目里的生命周期管理实践最后分享一个我比较成型的管理方案。我会把一个模块里的生命周期回调集中到一个专门的配置类里而不是散落在每个 Bean 中。例如数据库模块的初始化、清理、健康检查统一写到DataSourceLifecycleConfig类中通过Bean(initMethod init, destroyMethod close)声明。这样做的最大好处是整个应用有多少个生命周期钩子一目了然排查问题时不用满项目找PostConstruct。同时我会给每个初始化回调加一个简单的计时日志例如log.info(DataSource init complete, cost{}ms, cost)。这个习惯帮我抓了很多启动缓慢的问题某次要中心接入一个外部服务容器启动突然从 3 秒变成 15 秒通过各个初始化日志的耗时一秒就定位到是某个 Feign 客户端的初始化去做了远程健康检查。你看生命周期这个知识点平时没什么存在感但真到排查启动问题、容器关闭问题时它就是第一把排查钥匙。说回文章开头那个定时任务不执行的案例。最后我们查到原因是因为那个任务的PostConstruct里做了远程配置拉取第一次启动时网络超时了一次初始化异常导致任务 Bean 没注册成功。但更上层还有一个BeanPostProcessor在处理其他 Bean 时 catch 掉了这个异常打了 warning 日志这正好解释了为什么启动“看起来成功”而任务没起来。后来我们把超时时间缩短、加了重试并且把这个初始化逻辑挪到了ApplicationReadyEvent里问题彻底解决。每次回想起这个排查过程我都更加确信Spring Bean 生命周期不只是一个面试考点它是一张你定位问题时的地图越早把地图刻在脑子里踩坑的代价就越小。
返回列表