ARTICLE DETAIL

资讯详情

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

Spring容器生命周期全解析:从BeanDefinition加载到优雅停机

Spring容器生命周期全解析:从BeanDefinition加载到优雅停机 Spring容器这东西天天在用但你真的把它玩明白了吗很多人能熟练写Component、Autowired张口闭口依赖注入、控制反转但被问一句Spring容器是什么时候创建的谁创建的关闭的时候做了哪些事就蔫了。这不怪你因为大多数业务开发根本接触不到容器启停的临界点Spring Boot把这一切都藏得太好了。但只要你做过中间件封装、写过公共starter、搞过对稳定性要求极高的长跑服务或者单纯想在面试聊Spring时多撑两轮容器的生命周期管理就是你绕不过去的必修课。这篇文章我打算从容器启动前的准备讲起一路跟到容器关闭后的打扫战场再把Spring Boot优雅停机、Spring AI这类扩展组件如何搭上容器生命周期顺风车、以及多人踩坑的细节全翻出来。既有源码定位也有可直接抄走的配置和实践经验。看完你再去调服务、写组件、面试扯淡脑子里会多一张完整的容器从生到死地图。1. 容器启动前夜Spring的装配地图是怎样被建立起来的先别急着看refresh()我们要从一个更底层的问题开始Spring容器启动启动的到底是什么你可以把Spring容器想象成一个大型餐厅。餐厅开业容器启动之前必须有菜单、有食材清单、有供应商名单、有厨师和服务员的排班表。对Spring来说这些就是BeanDefinition——每个Bean的档案里面记录了类名、作用域、是否懒加载、初始化方法、销毁方法、依赖项等所有装配所需的信息。1.1 BeanDefinition的加载路径与扩展入口Spring容器启动的第一件大事就是收集足够的BeanDefinition。不同项目形态收集方式不一样基于XML的年代靠ClassPathXmlApplicationContext读取XML里的bean标签。基于注解的年代靠AnnotationConfigApplicationContext扫描包路径把带Component、Service、Repository、Controller标注的类解析成BeanDefinition。Spring Boot年代靠SpringBootApplication里复合的ComponentScan加EnableAutoConfiguration后者通过AutoConfigurationImportSelector从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里批量导入自动配置类。这个阶段最值得留意的扩展点是BeanFactoryPostProcessor它可以在Bean实例化之前修改BeanDefinition的定义。典型代表是PropertySourcesPlaceholderConfigurer负责把${...}占位符解析成真实配置值。还有ConfigurationClassPostProcessorSpring注解驱动体系的核心没有它Configuration、Bean、ComponentScan全是废纸。我实际写中间件时经常用BeanDefinitionRegistryPostProcessor在容器启动最早期动态注册BeanDefinition。这里有个细节BeanDefinitionRegistryPostProcessor继承自BeanFactoryPostProcessor它先执行且能拿到BeanDefinitionRegistry意味着你能在这个阶段手动注册全新的Bean定义。比如你想把某个外部SDK的客户端自动包装成Spring Bean又不想让使用方手动写Bean这就是最佳切入点。1.2 配置类的处理顺序为什么你的Bean方法也能被增强这里我要插一个很多人会忽略的点Configuration类里的Bean方法默认用的是CGLIB代理不是直接调用原方法。为什么为了保证Bean方法内部调用另一个Bean方法时返回的是容器里的单例Bean而不是new一个新对象。这就是Full模式和Lite模式的差别。ConfigurationClassPostProcessor在解析配置类时会把Configuration标记的类做CGLIB增强。如果你的配置类被Configuration(proxyBeanMethods false)关闭了增强那么内部手工调用Bean方法时就会new出新的实例脱离容器管理。这个坑我见过不止一次——某个团队把配置类上的proxyBeanMethods设为false后依赖了内部方法调用返回值的Bean每天报不是单例排查半天才找到原因。所以容器启动的前夜本质上是在搭建一张装配地图。地图清晰、扩展入口被正确触发后面的实例化阶段才能顺风顺水。如果这一阶段出了岔子报错常常是BeanDefinitionStoreException或者BeanDefinitionParsingException定位思路应该是先看配置类是否被扫到、BeanFactoryPostProcessor是否被正确注册、条件装配Conditional是否成立。2. 容器正式开门营业从BeanFactory到ApplicationContext的跃迁BeanDefinition收集好之后容器还只是个空壳真正的重头戏是AbstractApplicationContext的refresh()方法。这个方法的名字起得非常传神刷新。它内部调用了一整套流程把空壳容器变成五脏俱全的ApplicationContext。2.1 refresh()方法里的13个关键步骤我直接给你梳理refresh()里最影响理解的步骤prepareRefresh()准备刷新设置容器启动时间、活跃状态初始化属性源。obtainFreshBeanFactory()这一步决定了子类如何创建底层DefaultListableBeanFactory并加载BeanDefinition。prepareBeanFactory()给BeanFactory配置标准特性比如类加载器、表达式解析器、资源编辑器注册以及一堆特殊的BeanPostProcessor。postProcessBeanFactory()模板方法给子类机会增加后置处理器。invokeBeanFactoryPostProcessors()执行所有BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor。这是容器启动阶段最重要的扩展点之一Spring Boot的自动配置就是在这里被解析的。registerBeanPostProcessors()注册所有BeanPostProcessor注意这里只是注册还没执行。initMessageSource()初始化国际化相关的MessageSource。initApplicationEventMulticaster()初始化事件广播器。onRefresh()模板方法子类扩展用。Spring Boot的ServletWebServerApplicationContext就是在这里启动了内嵌的Tomcat、Jetty或Undertow。registerListeners()注册实现了ApplicationListener的Bean作为监听器。finishBeanFactoryInitialization()这里就是容器真正开门营业的时刻——实例化所有剩余的单例Bean。finishRefresh()发布ContextRefreshedEvent事件启动生命周期处理器。resetCommonCaches()清理反射缓存等。2.2 扩展点与内置组件的入驻顺序为什么必须是这个顺序你发现没有整个refresh()的执行顺序是有讲究的先让后置处理器和监听器就位再去实例化普通业务Bean。为什么因为普通Bean实例化过程中极有可能产生事件、需要后置处理器介入如果顺序反了事件发了没人收后置处理器登场了也晚了。我在封装组件时经常利用onRefresh()这个钩子。Spring Boot的WebServer就是在这里启动的所以你在ApplicationRunner或ApplicationListenerApplicationReadyEvent里访问HTTP端口一定是安全的。SpringApplication.run()返回的ConfigurableApplicationContext在此时已经全部初始化完成。你拿它getBean()出来的对象全是经历过完整生命周期管理的成品。这就是为什么Spring容器开启不只是new ApplicationContext()那么简单——它要走过这一整套精密的流水线。3. 单例Bean的实例化工程三级缓存与循环依赖的真实作用finishBeanFactoryInitialization()是容器启动阶段最核心、也最耗时的一步因为它会实例化所有非懒加载的单例Bean。你以为Spring是用到再创建并不是。Spring的默认策略是在容器启动阶段就把所有单例Bean全部new出来这也是为什么Spring Boot应用启动慢很大一部分时间耗在这。3.1 从实例化前到初始化完成一个Bean的生命周期路径单个Bean的完整生命周期比多数人想象的复杂得多。我按执行顺序画个逻辑链InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()如果你在这里能返回一个代理对象后面的实例化流程直接短路。构造函数实例化此时Bean对象已经存在但属性还没赋值。MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()解析Autowired、Value、Resource等注入点。InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()可以决定要不要走属性填充。InstantiationAwareBeanPostProcessor.postProcessProperties()执行真正的依赖注入。BeanPostProcessor.postProcessBeforeInitialization()执行PostConstruct、InitializingBean等之前的一系列前置处理。initializingBean.afterPropertiesSet()init-method正式初始化。BeanPostProcessor.postProcessAfterInitialization()执行AOP代理创建的黄金时机。放入单例池singletonObjects。这中间任何一环抛异常容器启动就会直接失败。所以排查启动报错时看到一个BeanCreationException别慌顺着Caused by一层层往下翻最终根因往往在某个postProcessBeforeInitialization或构造器里。3.2 Spring三级缓存到底在解决什么问题之前有很多人问Spring三级缓存原理我这里用一个餐厅换菜的例子讲透。假如餐厅有两道菜菜A需要厨师甲菜B需要厨师乙。正常情况下没问题但假如菜A的菜谱写着需要厨师乙来调味菜B的菜谱写着需要厨师甲来摆盘这就形成了循环依赖做菜A时要找厨师乙可厨师乙必须等菜B做完才能开始做菜B时要找厨师甲可厨师甲必须等菜A做完才能赴约。Spring的三级缓存解决的是单例Bean在属性填充阶段发生循环依赖的问题。三级缓存分别是一级缓存singletonObjects存放成品Bean。二级缓存earlySingletonObjects存放半成品Bean已实例化但未完成属性填充的Bean。三级缓存singletonFactories存放ObjectFactory用于提前生成Bean的早期引用。核心逻辑是A创建时发现自己依赖B先去三级缓存里放一个A的ObjectFactory创建B时发现B依赖A从三级缓存中找到A的工厂生成A的早期引用放到二级缓存B拿到A的早期引用完成属性填充走完生命周期回头A再拿到B的成品完成自己的填充。整个过程关键点在于宽容允许半成品先拿去用后面再补全。值得强调的是三级缓存只解决单例且非构造器注入的循环依赖。如果你用构造器注入A的构造器需要BB的构造器需要A两边都还没完成实例化谁都拿不到对方的半成品必然报BeanCurrentlyInCreationException。很多人不理解为什么Spring官方推荐构造器注入却还会遇循环依赖其实就是因为构造器注入在三级缓存下无解此时应该重构设计而不是继续改注入方式。3.3 懒加载与大厂为什么不喜欢开它再补一个关于Lazy的点。Lazy标注的Bean不会在容器启动阶段实例化而是等到第一次被使用时才创建。它能加速启动但副作用是错误会被推迟到运行时才暴露而且首次访问卡顿会直接影响线上体验。我个人的观点除非确实有启动时间硬指标、且要懒加载的Bean本身初始化成本极高否则尽量不要把核心服务Bean设为懒加载。启动慢几秒可以接受线上运行期突然卡死几秒不能接受。4. 容器关闭一场安静但必须有序的下班大扫除容器开启时轰轰烈烈容器关闭时很多人以为就是程序结束了一切交给操作系统。大错特错。Spring容器关闭需要完成一系列扫尾工作发布关闭事件、销毁所有单例Bean、关闭底层资源、断开连接池、释放文件句柄。如果你开发的服务在停机时经常日志突然中断、连接没释放干净、或者优雅停机老是不生效多半是没搞懂容器关闭时发生了什么。4.1 close()方法内部的执行链条AbstractApplicationContext.close()的核心流程如下加锁防止并发关闭。publishEvent(new ContextClosedEvent(this))发布容器关闭事件。所有监听ContextClosedEvent的监听器会在这里被触发。destroyBeans()调用beanFactory.destroySingletons()销毁所有单例Bean。closeBeanFactory()标记BeanFactory已关闭。onClose()给子类留的钩子GenericWebApplicationContext等Web容器实现会在这里关闭底层WebServer。liveBeansView.clear()清理JMX相关视图。流程看起来简单但每一环都可能藏雷。比如ContextClosedEvent发布时如果某个监听器异常会不会影响后续Bean销毁答案是Spring采集了所有监听器异常最终会继续执行后续关闭流程。这个设计很聪明——保证容器关闭不能被一个不听话的监听器卡死。4.2 Bean销毁的顺序PreDestroy、DisposableBean、destroy-method谁先谁后单个Bean的销毁顺序遵循一个固定的优先级链标注了PreDestroy的方法。实现DisposableBean接口的destroy()方法。Bean(destroyMethod ...)或XML里配置的destroy-method。顺序规则是越靠近程序员代码的越先执行越靠近框架约定的越后执行。为什么要这个顺序因为PreDestroy是JSR-250规范语义上是业务方自定义清理DisposableBean是Spring框架接口destroy-method属于配置驱动。先执行自定义清理再执行框架级别清理逻辑上更合理。踩坑提示如果你在PreDestroy里用了ApplicationContext.getBean()再去访问别的Bean有可能那个Bean已经被销毁了。因为容器销毁单例Bean时是按照注册顺序逐个来的不是等全部销毁完再统一通知。所以在销毁方法里做跨Bean协作是不靠谱的能省则省。4.3 shutdownHook怎么保证你点停止时容器真的走了优雅流程Spring Boot应用直接kill -9当然不会被拦截但常规的kill命令SIGTERM会触发JVM的Shutdown Hook。ConfigurableApplicationContext接口里有个registerShutdownHook()方法Spring Boot在SpringApplication.run()里就已经自动调用过了所以你的Spring Boot应用收到SIGTERM时虚拟机会回调这个钩子进而触发close()。这个机制有个隐藏彩蛋你可以手动调用registerShutdownHook()多次但Spring用的是Thread去注册且同一个ApplicationContext多次注册时Spring会先移除之前的钩子再注册新的不会导致关闭逻辑重复执行。这细节很多源码分析文章都没提过。如果你自己做裸的AnnotationConfigApplicationContext想优雅关闭逻辑等价于AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); context.registerShutdownHook(); // 业务代码 // JVM退出时会自动触发context.close()手动关闭则直接调用context.close()。但要小心close()之后容器就进入不可逆的关闭状态你再调getBean()会直接抛IllegalStateException。5. Spring Boot的优雅停机与容器关闭策略实战这部分我讲讲实战中最常见的三块优雅停机配置、WebServer关闭顺序、以及自定义SmartLifecycle时经常踩的坑。以前这些内容都散落在Spring Boot文档和源码注释里这次整合到一起。5.1 优雅停机server.shutdowngraceful的正确打开方式Spring Boot 2.3.0之后支持优雅停机核心配置就一行server.shutdowngraceful配合可选的超时时间spring.lifecycle.timeout-per-shutdown-phase30s开启优雅停机后Tomcat收到SIGTERM时不再立刻拒绝新请求而是进入停止阶段等待正在处理的请求完成超过timeout-per-shutdown-phase则强制关闭。这才叫有尊严地下班。但有个容易忽略的前提这个配置只对Servlet容器生效。如果你用的是Reactive WebWebFlux的Netty同样配置一样生效但底层机制是Netty的优雅关闭原理类似。还有个细节Spring Boot的优雅停机是在SmartLifecycle的stop()回调里阻塞等待的。spring.lifecycle.timeout-per-shutdown-phase控制的就是每个生命周期阶段的超时时间。因此如果你自己也有SmartLifecycle组件它们的停机总耗时会被这个配置约束。5.2 从web server到业务Bean谁的关闭顺序在前你可能以为既然是业务优先那应该业务Bean先销毁、WebServer后关闭实际顺序恰好相反首先容器发布ContextClosedEvent。接着SmartLifecycle组件按getPhase()降序执行stop()WebServer的停止器就在这一环。然后之后才执行普通单例Bean的销毁。也就是说WebServer先停止接收新请求此时业务Bean仍然活着还可以处理当前已进入的请求。等处理得差不多了才进入Bean销毁阶段把连接池、消息队列、缓存客户端挨个关掉。这个顺序对设计停机逻辑非常重要你的消息监听容器比如RabbitListener、KafkaListener如果也是SmartLifecyclePhase越小越早停止。实际项目里我一般把消息消费暂停器的Phase设得比较低让它先于普通Bean销毁前停止拉取消息避免销毁过程中还不断有新消息进来触发对已销毁组件的调用。5.3 SmartLifecycle的Phase排序和停机竞态SmartLifecycle接口里有个getPhase()方法数值越大表示启动阶段越靠后关闭阶段越靠前。是反的。这个设计坑过不少人。启动时按Phase从小到大启动。关闭时按Phase从大到小关闭。如果你有一堆SmartLifecycle要理清楚停机顺序永远先想关闭时谁先停。我的经验是按依赖方向排依赖底层的先启动、后关闭依赖上层的后启动、先关闭。消息消费者必须等在它下面的连接池销毁之前先停止否则连接池一关消费者还在拉消息一拉一个错。另外提一句如果你的容器里同时存在SmartLifecycle和普通Bean(destroyMethodclose)两者的关闭顺序在Spring Boot里是有保证的——LifecycleProcessor的onClose()是在close()方法里先于destroySingletons()被调用的所以SmartLifecycle的stop()一定先于Bean的destroy()。5.4 一个真实案例Feign Client在停机时为什么偶尔报连接失败我之前遇到过一个问题服务在发布滚动更新时偶尔有流量打到正在下线的实例上导致Feign接口报连接失败。根源是Kubernetes的优雅终止流程Pod进入Terminating状态后先发SIGTERM等容器优雅退出超时后强杀。但我配了server.shutdowngraceful为何还报错排查后发现K8s的preStop钩子没配容器一收到SIGTERM就立刻从Service Endpoints里摘除但此时已经进入Tomcat优雅停止等待问题出在客户端没配置足够长的重试和连接超时而Tomcat等着处理已有请求新请求虽然不接收但已建立连接还没断开的请求在排队。真正的解法是组合拳preStop里加个sleep等待从Endpoints摘除再配合server.shutdowngraceful和spring.lifecycle.timeout-per-shutdown-phase调大一点。再配合Feign的Request.Options把connectTimeout和readTimeout调短让它快速失败重试到新实例。这样丢掉的只有个别超时请求不会大面积错误。这就是为什么我强调优雅停机不是某一项配置能解决的它是容器编排、应用生命周期、服务发现、调用超时四层联动的结果。6. 扩展组件的顺风车Spring AI、Starter、自定义监听器如何感知容器启停最后一个模块也是很多做中间件、AI应用集成的人最关心的如何让你的组件搭上容器生命周期的顺风车在正确的时机启动、正确的时机关闭。6.1 Spring AI等外部集成是如何自动装配并参与生命周期的Spring AI是近期热门的AI应用集成框架它提供了ChatClient、EmbeddingModel等组件底层通过Spring Boot自动装配机制注入容器。你引入spring-ai-starter后它自动装配的ChatClient就是一个容器Bean。这个东西为什么会开箱即用原因就是它走了AutoConfiguration在自动配置类里用Bean向容器注册了相关组件。对容器生命周期的感知Spring AI这类库通常通过以下方式参与实现InitializingBean或PostConstruct初始化模型连接、检查API Key。实现DisposableBean或PreDestroy关闭HTTP连接池、清理线程池。实现ApplicationListenerContextRefreshedEvent在容器完整启动后再执行某些就绪动作。实现SmartLifecycle需要精确控制启停阶段时。我这里给你个自定义组件的模板把生命周期和ApplicationContext打通Component public class AIClientLifecycle implements SmartLifecycle, ApplicationContextAware { private volatile boolean running false; private ApplicationContext applicationContext; Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext applicationContext; } Override public void start() { // 容器refresh完成后执行初始化连接池、加载模型配置等 this.running true; } Override public void stop() { // 关闭之前先释放资源关闭HTTP客户端、清空本地缓存 this.running false; } Override public boolean isRunning() { return this.running; } Override public int getPhase() { return Integer.MAX_VALUE; // 启动最晚关闭最早 } }在Spring AI的集成中如果你需要基于AI能力做自定义RAG服务这个模式可以让你在start()里初始化向量数据库连接在stop()里释放保证业务Bean使用AI组件时底层资源一定已就绪。6.2 开发自己的Spring Boot Starter时生命周期扩展点怎么选如果你要开发自己的Starter我建议按以下规则选扩展点想调整BeanDefinition用BeanDefinitionRegistryPostProcessor。想在容器启动早期改配置用BeanFactoryPostProcessor。想拦所有Bean的创建、做代理增强用BeanPostProcessor。想在容器整体就绪后做事监听ApplicationReadyEvent或ContextRefreshedEvent。想在停机时做资源清理实现DisposableBean、PreDestroy或SmartLifecycle。想感知WebServer启动在WebServerInitializedEvent里处理。一个很容易踩的坑是在PostConstruct里启动一个定时任务结果容器没完全起来任务却已经开始跑访问别的Bean时报错。正确做法是延迟到ApplicationReadyEvent再启动任务。反过来停机的清理放到PreDestroy没问题但如果你用了Spring SchedulingScheduled任务背后的线程池是Spring自己管理的它的关闭时机在容器Bean销毁过程中是被安排好的你不需要手动去停线程池除非你自定义了TaskScheduler。6.3 自定义事件让业务模块优雅适应容器重启我再分享一个项目里用过多次的设计通过监听容器生命周期事件实现模块级就绪/下线状态切换。比如你有个网关模块需要在下线时先从注册中心注销再休眠几秒让上游不再转发流量然后才关闭。这种场景下只靠PreDestroy是不够的因为在PreDestroy执行时容器的WebServer很可能还没关流量还在进。正确做法是监听ApplicationListenerContextClosedEvent在事件里执行注销注册中心短暂sleep置为下线状态。Component public class GracefulOfflineListener implements ApplicationListenerContextClosedEvent { Override public void onApplicationEvent(ContextClosedEvent event) { // 1. 调用注册中心下线接口 registry.deregister(); // 2. 给上游服务几秒钟感知 try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }注意这个逻辑要尽量快因为ContextClosedEvent同步发布后面的Bean销毁流程在等它。能异步处理的就不要同步sleep非阻塞优先。如果你确实需要等待上游摘除时间一定要短或者在SmartLifecycle里结合停机超时一起设计。7. 手写Spring时最值得抄的容器启停骨架设计说到这很多人会想既然这么复杂那我理解它的最好方式是不是自己写一个迷你Spring容器确实是的。把启停流程亲手撸一遍比背十遍源码都管用。这里我给你一个极简容器骨架它虽然精简但把生命周期的精华都保留了。7.1 一个最简Bean容器的开启流程设计思路用一个MapString, Object当单例池一个MapString, BeanDefinition当注册表。开启时做三件事解析配置、创建Bean、放进单例池。public class SimpleApplicationContext { private final MapString, Object singletonObjects new ConcurrentHashMap(); private final MapString, BeanDefinition beanDefinitionMap new ConcurrentHashMap(); private final ListBeanPostProcessor postProcessors new ArrayList(); public void refresh() throws Exception { // 1. 解析BeanDefinition loadBeanDefinitions(); // 2. 注册BeanPostProcessor registerBeanPostProcessors(); // 3. 实例化所有非懒加载单例Bean finishBeanFactoryInitialization(); // 4. 发布刷新完成事件 publishEvent(new ContextRefreshedEvent()); } private void finishBeanFactoryInitialization() throws Exception { for (String beanName : beanDefinitionMap.keySet()) { // 循环依赖先不管这里只做完整创建 if (!singletonObjects.containsKey(beanName)) { Object bean createBean(beanName, beanDefinitionMap.get(beanName)); singletonObjects.put(beanName, bean); } } } private Object createBean(String beanName, BeanDefinition bd) throws Exception { Object bean bd.getBeanClass().getDeclaredConstructor().newInstance(); // 属性填充简化 applyPropertyValues(bean, bd); // 初始化前处理 for (BeanPostProcessor bpp : postProcessors) { bean bpp.postProcessBeforeInitialization(bean, beanName); } // 初始化 if (bean instanceof InitializingBean) { ((InitializingBean) bean).afterPropertiesSet(); } // 初始化后处理 for (BeanPostProcessor bpp : postProcessors) { bean bpp.postProcessAfterInitialization(bean, beanName); } return bean; } }这个片段虽然简单但你注意我保留了BeanPostProcessor的处理顺序实例化后、属性填充后、初始化前、初始化后、初始化后处理。这其实就是Spring Bean生命周期里最核心的骨架。你把这个跑通再去看Spring源码里的AbstractAutowireCapableBeanFactory.doCreateBean()会发现主干几乎一模一样只是它多了缓存、循环依赖、类型转换、AOP织入等一堆细节。7.2 关闭流程应该怎么设计关闭流程的核心是让Bean有机会释放资源同时保证事件被通知到。仿照Spring我们需要在关闭时做这几件事public void close() { // 1. 发布关闭事件 publishEvent(new ContextClosedEvent()); // 2. 销毁单例Bean destroyBeans(); // 3. 标记已关闭 this.closed true; } private void destroyBeans() { // 按注册的逆序销毁模拟销毁顺序与创建顺序相反的语义 for (String beanName : beanDefinitionMap.keySet()) { Object bean singletonObjects.get(beanName); if (bean instanceof DisposableBean) { ((DisposableBean) bean).destroy(); } } singletonObjects.clear(); }这里有个细节值得说Spring源码里单例Bean存的是一个DisposableBeanAdapter它在destroy()里会依次调用PreDestroy、DisposableBean.destroy()、destroyMethod。这保证了多种销毁方式并存时的执行顺序。7.3 从手写容器里悟出的两条实战体会第一生命周期管理最重要的不是知道有哪些钩子而是理解每个钩子出现的时机。同一段逻辑放错钩子可能带来完全不同的效果。比如初始化连接池放在构造函数里和放在afterPropertiesSet()里看起来差不多但在代理、继承、循环依赖等场景下天差地别。Spring官方推荐在afterPropertiesSet()里做事而不是构造函数因为构造函数执行时对象还没完全准备好依赖也还没注入。第二容器关闭的容错设计极其重要。Spring在处理Bean销毁时某个Bean抛异常并不会中断其他Bean的销毁。如果你自己设计容器一定要继承这个思路销毁逻辑彼此隔离一个失败不影响整体清理。线上部署时如果一个Bean清理失败导致整个停机卡死那是灾难。8. 容器生命周期管理的最后补充从单测到线上怎样验证你的启停逻辑是对的掌握了前面所有原理和代码最后我们聊聊验证。生命周期逻辑不像业务代码那样点两下就能测出来它发生在容器启动和关闭的临界点容易踩看起来没跑、其实跑了但顺序不对的模糊地带。我分享一下我常用的验证手段。8.1 单元测试用Spring TestContext拿到完整生命周期想让测试覆盖容器启停最简单的办法是直接用SpringApplication或AnnotationConfigApplicationContext在测试里手动启停Test void lifecycleTest() { try (ConfigurableApplicationContext context new AnnotationConfigApplicationContext(TestConfig.class)) { MyService service context.getBean(MyService.class); assertNotNull(service); // 验证初始化已执行 assertTrue(service.isInitialized()); } // try-with-resources自动close这里可以验证销毁逻辑的效果 // 比如某个静态缓存被清空、某个连接被关闭 }如果是Spring Boot项目用SpringBootTest时容器是由Spring TestContext管理的它会在单测结束时自动关闭。你可以在测试里断言某些PreDestroy标记的静态状态已经被清理。但注意不要在单测里依赖静态状态做断言,因为多个测试类共享JVM静态变量会因为测试顺序不同产生偶发问题。真要验证销毁逻辑我建议直接在单个测试方法里手动new容器像上面的代码一样。8.2 集成验证把优雅停机当成发布流程的一等公民线上验证容器关闭我推荐三步走配置server.shutdowngraceful和sprinng.lifecycle.timeout-per-shutdown-phase后发一个SIGTERM给进程观察日志里是否存在Commencing graceful shutdown和Completed graceful shutdown两条记录。在关闭过程中持续发请求确认已接收的请求能正常返回新请求被拒绝返回503或连接重置无请求被硬中断如果被硬中断说明超时时间不够。配合注册中心观察实例下线状态确认下线逻辑在容器关闭前已完成。我见过一个团队上线时把spring.lifecycle.timeout-per-shutdown-phase设成2秒结果长请求一多Tomcat强制关闭线上偶发报错。后来调整到30秒配合preStop sleep 5秒基本解决问题。永远根据你自己业务请求的最大耗时来定超时时间不要照抄网上配置。8.3 通过Actuator和日志判断容器状态Spring Boot Actuator的/actuator/health可以反映应用就绪状态但它默认只反映组件的健康检查不反映容器正在关闭中。如果你需要在关闭期间让健康检查返回DOWN让负载均衡器把实例摘掉可以自定义一个HealthIndicator它在ContextClosedEvent发布后返回DOWNComponent public class ShutdownStateHealthIndicator implements HealthIndicator { private final AtomicBoolean shuttingDown new AtomicBoolean(false); EventListener public void onShutdown(ContextClosedEvent event) { shuttingDown.set(true); } Override public Health health() { if (shuttingDown.get()) { return Health.down().withDetail(reason, container is stopping).build(); } return Health.up().build(); } }配合注册中心的健康检查频率这能让流量在容器真正关闭前就避开这个实例极大降低停机期间的报错率。这个设计思路非常实用在一些对可用性要求极高的支付、交易系统中这个自定义健康检查是标配。到这里Spring容器的开启与关闭从BeanDefinition加载、refresh流水线、单例Bean实例化与循环依赖到关闭事件、Bean销毁、优雅停机、扩展组件整合、手写骨架、验证思路一条线全串起来了。最后说一句正文里反复验证过的体会容器生命周期管理本质上就是时机管理。什么逻辑该在什么时机执行决定了你整个应用在启动和停止时的稳定性和优雅程度。理解了这一点你写中间件、调发布流程、修诡异报错都会顺手很多。
返回列表