Spring Bean生命周期:五步七步十步与下单链路

发布时间:2026/10/31 23:12:23
Spring Bean生命周期:五步七步十步与下单链路 1. 先搞清楚下单这条链路为什么能把Bean生命周期串起来面试被问Bean生命周期几乎所有人都是张口就来实例化、属性赋值、初始化、使用、销毁然后面试官只要往下追问一句初始化阶段到底包含几个回调顺序是什么脑子里的记忆链条就会开始松动。我自己当年也是这样把五步法、七步法、十步法抄在笔记本上背了三天隔一周再看发现只记住了中间有个BeanPostProcessor具体卡在哪个缝隙里完全说不出来。问题不在记性在于这些步骤是脱离场景的抽象标签。人的大脑对标签序列的记忆效率极低但对有因果关系的流程记忆力强得离谱。你会忘记Bean生命周期第九步是什么但你绝不会忘记下单要先扣库存、再扣款、最后发通知因为那是一条有前后依赖的业务链路。所以这篇内容的核心思路是把Spring容器创建一个Bean的过程完整映射到用户下单时订单服务这个对象怎么被造出来、怎么被塞进依赖、怎么被包上事务代理、最后怎么被关掉。Bean生命周期的五步、七步、十步本质上不是三套知识而是同一根链条的三种颗粒度——五步是业务视角七步是框架视角十步是中间件/扩展开发视角。你之所以背不住是因为你一直用业务视角去硬啃中间件视角的细节。这篇文章适合三类人写过Spring业务代码但没深挖过容器的准备面试想一次讲清楚的以及要写自定义starter、写BeanPostProcessor做埋点或权限校验的。代码基于Spring 6.1 JDK 17注解全部使用jakarta.annotation包老项目用的是javax.annotation这一点后面会专门说。2. 五步法能跑起来的最小闭环也是面试的保底答案2.1 五步到底指的是哪五步社区里流传最广的五步法是把整个链路压缩到业务代码能感知到的粒度步骤官方叫法在下单场景里对应什么1实例化Instantiation从图纸造出一个空的OrderService对象壳子2属性赋值Populate把库存服务、支付服务塞进这个壳子3初始化Initialization执行业务自检逻辑比如预热汇率表4使用In Use真正调用createOrder()下单5销毁Destruction容器关闭时释放连接池、刷盘这五步之所以好记是因为它对应的是对象从生到死的自然直觉。但如果只停在这里你其实什么也没学到——因为这五步换个说法就是new 一个对象、set 几个字段、调一下 init、用、调一下 destroy任何一个手写工厂都能实现Spring 的价值一点都没体现出来。2.2 用下单场景把五步走一遍假设我们有个订单服务创建订单需要两个协作者库存服务负责预占库存支付服务负责发起扣款。容器启动时OrderService这个Bean要被造出来。第一步实例化Spring 通过反射找到构造器。这里有个很多新手会忽略的细节如果你的OrderService只有一个带参构造器比如OrderService(InventoryService, PaymentService)Spring 4.3 之后会走隐式构造器注入不需要写Autowired。但如果这个类有两个以上构造器Spring 就不知道该用哪个必须显式标注否则直接抛NoUniqueBeanDefinitionException。我在实际项目里见过有人给类加了 Lombok 的RequiredArgsConstructor之后又手写了一个无参构造器结果容器启动直接失败排查了半小时。第二步属性赋值注意这里填的是壳子里的属性。构造器注入的参数其实在第一步就已经传进去了属性赋值这一步主要处理Autowired字段和 setter 注入。所以严格意义上说构造器注入的依赖在实例化阶段就到位了字段注入的依赖在属性赋值阶段才到位——这个差异后面讲循环依赖的时候会变成关键。第三步初始化这是最容易出问题的一步。业务上我们经常需要Bean 的所有依赖都就绪之后再做点什么比如加载一次全量商品价格快照、建立一次长连接探活。如果你把这些逻辑写在构造器里依赖可能还没注入完直接空指针写在PostConstruct里就安全因为容器保证它在这个时点被调用。第四步使用这一步没什么好说的就是你的 Controller 调到 Service 的那一刻。第五步销毁容器close()时触发。注意只有单例Bean才会被容器管理销毁prototype作用域的Bean被创建出来之后就交给调用方了容器不再持有引用销毁回调压根不会执行。这个坑后面第四节会展开。2.3 五步法够用和不够用的分界线我给自己定了一条判断线只要你只是写业务代码五步法就够用甚至不需要知道七步。你只需要记住三件事构造器里别写重逻辑、依赖就绪后的初始化放PostConstruct、资源释放放PreDestroy。这三条能覆盖 90% 的日常开发。但只要你开始碰下面任何一种场景五步法就会失效需要给一批Bean统一加日志埋点、需要动态代理某个Bean、需要写自定义注解让容器识别、排查为什么我的AOP在同类内部调用失效。这些场景的共同点是——你需要知道Bean在什么时刻还没被代理又在什么时刻已经被代理。这个信息五步法给不了你得升到七步。3. 七步法把 BeanPostProcessor 的位置标出来AOP 才不再是黑盒3.1 七步比五步多出来的两步是什么七步法的展开逻辑是把初始化这一个抽象步骤劈开露出里面的两个扩展点回调实例化属性赋值Aware 回调BeanNameAware、BeanFactoryAware等BeanPostProcessor.postProcessBeforeInitialization初始化方法PostConstruct→afterPropertiesSet→init-methodBeanPostProcessor.postProcessAfterInitialization销毁这两步扩展点是整个Spring生态的命脉。你要知道一件事Spring 里几乎所有魔法都是在第4步和第6步里做出来的。Autowired的注入靠第2步的postProcessPropertiesPostConstruct靠第4步里的CommonAnnotationBeanPostProcessor而AOP事务、Async、Cacheable的代理对象全部是在第6步生成的。这句话解释了无数人的困惑为什么Transactional方法在同一个类内部调用不生效因为容器里最终放进单例池的那个对象是第6步被包装后的代理对象而你this.xxx()调用的是原始对象自己的方法压根没经过代理。3.2 第6步的时机决定了代理的面貌我们拿下单链路具体走一遍。OrderService.createOrder()上标了Transactional容器在第6步用AbstractAutoProxyCreator判断这个Bean是否需要被代理——它扫描所有 advisor发现有一个BeanFactoryTransactionAttributeSourceAdvisor能匹配上于是生成一个 CGLIB 子类把原对象包进去。关键点在于代理对象是在初始化方法执行完之后才产生的。这意味着你的PostConstruct里如果this.xxx()调了一个带事务的方法事务不会生效因为此刻this还是原始对象代理还没生出来。反过来如果有人把事务逻辑写在PostConstruct里期望它回滚那更是想多了——初始化阶段压根没有事务上下文。我在一个项目里就踩过这个坑某个订单服务在PostConstruct里调用自身的事务方法做数据预热日志一切正常但出错时数据脏写没能回滚查了半天才定位到是代理时机问题。解决办法是把预热逻辑搬到ApplicationRunner或者监听ApplicationReadyEvent那时候容器里拿到的已经是代理对象了。顺带说一个第3步的细节很多人以为ApplicationContextAware和BeanNameAware是同一批回调其实不是。BeanNameAware、BeanClassLoaderAware、BeanFactoryAware这三个是在invokeAwareMethods()里硬编码调用的位置非常靠前而ApplicationContextAware、EnvironmentAware、ResourceLoaderAware这些是由ApplicationContextAwareProcessor这个BeanPostProcessor在第4步阶段处理的。所以严格来说它们不在同一层。这个区别在写自定义扩展时会有影响——如果你自己实现了一个BeanPostProcessor且优先级排到了ApplicationContextAwareProcessor前面那你在这个处理器里拿到的Bean可能还没拿到ApplicationContext。3.3 手写一个下单耗时统计的后置处理器光讲不如动手。假设产品经理要求统计每个订单服务的初始化耗时方便排查启动慢的问题。这就是典型的第4步/第6步扩展点场景Component public class InitCostPostProcessor implements BeanPostProcessor, Ordered { private final MapString, Long startTimeMap new ConcurrentHashMap(); Override public Object postProcessBeforeInitialization(Object bean, String beanName) { if (beanName.startsWith(order)) { startTimeMap.put(beanName, System.nanoTime()); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) { Long start startTimeMap.remove(beanName); if (start ! null) { long cost (System.nanoTime() - start) / 1_000_000; System.out.printf([启动耗时] %s 初始化花了 %d ms%n, beanName, cost); } return bean; } Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }这段代码有几个实操要点值得说第一必须返回bean本身不能返回 null。返回 null 会导致后续的 BeanPostProcessor 拿到 null直接空指针。我见过有人为了过滤某个Bean写了return null结果启动报了一堆莫名其妙的错。第二getOrder()返回LOWEST_PRECEDENCE是有意为之。因为 Spring 内置的处理器比如负责AOP的那个优先级通常很高你的埋点处理器排在最后测出来的耗时才包含前面的处理时间数据才有意义。如果你想测的是纯业务初始化耗时那就该排在最前面。第三startTimeMap用ConcurrentHashMap而不是HashMap。虽然默认单例Bean是串行创建的但如果你开启了spring.main.lazy-initialization或者有Async触发的懒加载就可能并发。这个细节属于防御性编程成本几乎为零。4. 十步法拆到 Aware 和销毁中间件开发者的视角4.1 十步的完整顺序与记忆锚点十步法是给要写框架扩展、写 starter、排查诡异问题的人准备的。完整的顺序是这样的序号阶段具体动作1实例化前InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation2实例化反射调用构造器 / 工厂方法3实例化后postProcessAfterInstantiation4属性赋值前postProcessPropertiesAutowired在这里注入5属性赋值应用PropertyValues6Aware 回调BeanNameAware→BeanClassLoaderAware→BeanFactoryAware7初始化前postProcessBeforeInitialization含ApplicationContextAware、PostConstruct8初始化afterPropertiesSet()→init-method9初始化后postProcessAfterInitializationAOP 代理在这里生成10销毁PreDestroy→DisposableBean.destroy()→destroy-method记忆锚点我推荐用一个比喻造车。第1步是看看有没有现成的车可以直接交付postProcessBeforeInstantiation如果返回非 null整个后续流程直接跳过这是短路机制第2步是焊车架第3、4、5步是装零件第6步是贴车牌、告诉这辆车归属哪个车主第7到9步是出厂检测、贴膜、上牌第10步是报废回收。第1步的短路机制特别值得单独说一句。postProcessBeforeInstantiation如果返回了一个非 null 的对象Spring 会认为你已经替我把Bean造好了直接跳到第9步做后置处理中间的实例化、属性赋值、初始化全部跳过。AbstractAutoProxyCreator就是靠这个机制处理某些特殊代理场景的。如果你自己写处理器时不小心在这里返回了对象会导致依赖注入完全不生效然后你会收获一堆空指针。4.2 初始化和销毁的内部顺序别再记混了第7和第8步合并起来就是初始化但内部的三个回调顺序是固定的很多人记反初始化顺序PostConstruct→afterPropertiesSet()→init-method为什么是这个顺序因为PostConstruct是由CommonAnnotationBeanPostProcessor在postProcessBeforeInitialization里通过反射调用的天然属于初始化前而afterPropertiesSet()是InitializingBean接口方法由invokeInitMethods()直接调用init-method排在最后是留给使用者的最后一道口子。销毁顺序PreDestroy→DisposableBean.destroy()→destroy-method逻辑是镜像的。PreDestroy由CommonAnnotationBeanPostProcessor的postProcessBeforeDestruction调用destroy()由适配器直接调destroy-method收尾。这里有个实操技巧如果你只想写一处初始化和一处销毁写PostConstruct/PreDestroy就够了理由有三不需要让类实现 Spring 的接口减少耦合、符合 JSR-330 标准换框架也能用、顺序最靠前/最靠后初始化最早、销毁最晚语义最清晰。InitializingBean唯一的优势是能拿到更精确的容器语义但绝大多数业务场景用不上。还有一个容易被忽略的点如果你用Bean(initMethod xxx)声明Bean而xxx方法不存在编译期不会报错只有运行到初始化阶段才会抛异常而且异常信息是NoSuchMethodException非常不直观。我现在的习惯是能用注解就不用initMethod字符串实在要用就在单元测试里覆盖一次启动。4.3 原型Bean的销毁谁来管这个问题在面试里出现的频率很高也很能区别人是不是真跑过。结论是prototype作用域的Bean容器不负责销毁。原因是容器压根不持有原型Bean的引用——每次getBean()都造一个新的交给你容器内部不会缓存所以close()的时候它根本不知道有这么个对象存在。PreDestroy、DisposableBean、destroy-method在原型Bean上全部失效。那业务上要释放资源怎么办两个办法。一是让调用方自己负责调用完手动调销毁方法但这在业务代码里很难保证不漏。二是实现DestructionAwareBeanPostProcessor在处理器里记录所有创建过的原型Bean引用容器关闭时统一清理。这个方案能解决问题但会引入内存泄漏风险必须保证引用能被正确移除。说白了原型Bean 需要释放的资源本身就是个设计味道很重的组合能避开就避开改成单例 连接池复用是更稳妥的做法。5. 实操搭一条能打出完整生命周期的下单链路5.1 环境与依赖只依赖spring-context就够了不需要 Spring Bootdependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version6.1.6/version /dependencyJDK 用 17 或以上。如果你从旧项目迁移过来第一个要改的是 importjavax.annotation.PostConstruct要换成jakarta.annotation.PostConstruct否则PostConstruct会被静默忽略——不报错、不生效、初始化方法压根不执行。这个坑我在迁移时踩过一次排查了整整一个下午因为日志里什么异常都没有只有依赖注入为 null这个间接症状。5.2 一个把全部回调都打点的 OrderService为了让顺序一目了然我把所有扩展点全实现一遍public class OrderService implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean { private final InventoryService inventoryService; private PaymentService paymentService; public OrderService(InventoryService inventoryService) { this.inventoryService inventoryService; System.out.println(1. 构造器拿到库存服务 inventoryService); } Autowired public void setPaymentService(PaymentService paymentService) { this.paymentService paymentService; System.out.println(2. setter注入拿到支付服务 paymentService); } Override public void setBeanName(String name) { System.out.println(3. BeanNameAware: name); } Override public void setBeanFactory(BeanFactory beanFactory) { System.out.println(4. BeanFactoryAware: beanFactory.getClass().getSimpleName()); } Override public void setApplicationContext(ApplicationContext ctx) { System.out.println(5. ApplicationContextAware); } PostConstruct public void postConstruct() { System.out.println(6. PostConstruct预热商品价格快照); } Override public void afterPropertiesSet() { System.out.println(7. InitializingBean.afterPropertiesSet); } public void customInit() { System.out.println(8. init-method: customInit); } public void createOrder(String userId) { inventoryService.deduct(userId); paymentService.pay(userId); System.out.println(9. 业务调用下单完成); } PreDestroy public void preDestroy() { System.out.println(10. PreDestroy); } Override public void destroy() { System.out.println(11. DisposableBean.destroy); } public void customDestroy() { System.out.println(12. destroy-method: customDestroy); } }配置类用Bean方式声明显式指定initMethod和destroyMethodConfiguration ComponentScan(com.demo.order) public class OrderConfig { Bean(initMethod customInit, destroyMethod customDestroy) public OrderService orderService(InventoryService inventoryService) { return new OrderService(inventoryService); } }启动类public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(OrderConfig.class); OrderService service ctx.getBean(OrderService.class); service.createOrder(U1001); ctx.close(); } }5.3 实测输出与逐行解读跑起来之后控制台输出的顺序是固定的1. 构造器拿到库存服务 com.demo.order.InventoryService1a2b3c 3. BeanNameAware: orderService 4. BeanFactoryAware: DefaultListableBeanFactory 2. setter注入拿到支付服务 com.demo.order.PaymentService4d5e6f 5. ApplicationContextAware 6. PostConstruct预热商品价格快照 7. InitializingBean.afterPropertiesSet 8. init-method: customInit 9. 业务调用下单完成 10. PreDestroy 11. DisposableBean.destroy 12. destroy-method: customDestroy注意这里第2和第3、4行的顺序反了这不是打印bug。原因是Autowired标注在 setter 上时注入发生在populateBean阶段十步法第4、5步而BeanNameAware、BeanFactoryAware的回调发生在initializeBean阶段十步法第6步。所以 setter 注入必然排在 Aware 回调之前。这个细节非常值得记下来——它告诉我们如果你在BeanNameAware里想用一个Autowired注入的字段是可以用的反过来如果你在 setter 注入方法里想用beanName那是拿不到的。我见过有人在 setter 里写日志想带上 beanName结果全是 null就是这个原因。而ApplicationContextAware排在 setter 之后、PostConstruct之前也符合前面的分析——它由ApplicationContextAwareProcessor处理属于初始化前阶段且这个处理器的优先级高于CommonAnnotationBeanPostProcessor。销毁阶段PreDestroy在最前是因为它由DestructionAwareBeanPostProcessor的postProcessBeforeDestruction触发在DisposableBeanAdapter.destroy()里第一个被调用。5.4 加一个代理看第9步的变化现在给createOrder加个Transactional同时引入spring-tx和spring-jdbc配置一个内存数据库。重新跑一次你会发现输出里多了一行提示8. init-method: customInit [代理生成] orderService 被包装为 CGLIB 代理 9. 业务调用下单完成这行代理生成可以自己写个 BeanPostProcessor 打印AopUtils.isAopProxy(bean)验证。它的位置证明了一件事代理对象是在初始化完成之后才被替换进单例池的。所以如果你在PostConstruct里调用了事务方法事务不会生效。想进一步验证的话在createOrder前后各打一行日志然后在外部调用service.createOrder()、在内部再调一次this.otherTxMethod()你会看到只有外部调用有事务日志。这就是七步法最直接的商业价值——它让你能预测而不是试错。6. 常见问题与排查技巧实录6.1 典型问题速查表现象大概率原因排查手段PostConstruct不执行用了javax.annotation包名检查 importSpring 6 必须是jakarta.annotation依赖注入为 null手动new的对象不受容器管理检查是否通过getBean()获取Transactional内部调用失效内部调用绕过代理拆分到另一个Bean或用AopContext.currentProxy()启动报循环依赖Spring Boot 2.6 默认禁止重构依赖别急着开allow-circular-references原型Bean的销毁方法不执行容器不管理原型Bean改用单例或自行管理生命周期构造器报NoUniqueBeanDefinitionException多个构造器未标注显式加Autowired到目标构造器上BeanPostProcessor 里拿到的是原始对象处理器优先级排在了代理生成之前调整getOrder()或用SmartInstantiationAwareBeanPostProcessor6.2 三个我踩过的坑坑一在 BeanPostProcessor 里注入了业务Bean导致整个切面失效。这个坑非常隐蔽。BeanPostProcessor本身也是Bean如果它依赖了某个业务Bean容器为了保证处理器能正常工作会提前创建那个业务Bean。但这时候其他 BeanPostProcessor 还没注册完导致提前创建的业务Bean绕过了 AOP 代理直接变成裸对象。表现就是这个Service的事务莫名其妙不生效。解法很简单BeanPostProcessor里不要注入业务Bean需要的话用ObjectProvider延迟获取或者用ApplicationContext按需getBean()。坑二循环依赖和构造器注入的组合。字段注入的循环依赖 Spring 能靠三级缓存解决提前暴露ObjectFactory但构造器注入的循环依赖无解因为对象还没造出来没法提前暴露引用直接抛BeanCurrentlyInCreationException。Spring Boot 2.6 之后默认还关闭了循环依赖引用所以你会看到双重报错。我的建议是看到循环依赖就去重构抽出第三个类或者用事件解耦别第一时间去改配置开关那是把定时炸弹往后推。坑三Bean方法的返回类型写成接口导致代理类型不匹配。如果Bean方法返回OrderApi接口类型但实际返回的是OrderServiceImpl而OrderServiceImpl上没有实现接口方法上的注解AOP 匹配就会出问题。这个坑的排查成本极高因为你看到的是注解明明写了但不生效。稳妥做法是Bean方法的返回类型和实际类型保持一致别为了面向接口牺牲可预测性。7. 选型与决策几个容易做错的地方7.1 五步、七步、十步日常该按哪套来想问题我给自己的使用规则是这样的写业务代码时脑内只装三件事构造器别做重活、依赖就绪后的初始化放PostConstruct、资源释放放PreDestroy。这套规则简单粗暴能覆盖绝大多数场景。写切面、写中间件、写 starter 时切到七步。核心就问一个问题我要操作的这个Bean此刻是原始对象还是代理对象答案取决于我在第4步还是第6步动手。排查诡异问题、写自定义BeanPostProcessor排序时切到十步。绝大部分注解不生效的问题本质都是你以为的执行时机和实际的执行时机错位了。这套分层思路的好处是你不会在写 CRUD 的时候被迫回忆postProcessProperties和postProcessAfterInstantiation的区别也不会在写框架的时候还停留在实例化、属性赋值这种粗粒度上。7.2 关于懒加载和提前初始化Lazy是个双刃剑。它的作用是把这个Bean的创建推迟到第一次使用时好处是加快启动速度坏处是把启动期的错误推迟到了运行期。我在一个项目里给某个订单相关服务加了Lazy解决启动慢的问题结果第一次真实下单时才爆出配置缺失监控直接告警。我的经验是懒加载只对确实不常用且创建成本高的Bean开比如某些冷门的报表导出服务。核心链路下单、支付、库存一律不要懒加载宁可启动慢两秒也要把错误暴露在发布阶段而不是业务高峰。另外如果你确实需要所有单例都创建完之后再做点什么用SmartInitializingSingleton.afterSingletonsInstantiated()它的执行时机在所有单例Bean初始化完成之后、容器刷新完成之前。这个点比PostConstruct更晚比ApplicationReadyEvent更早是个很好用但很少人用的扩展点。7.3 关于 Spring 6 的几个实际变化迁移到 Spring 6 时除了jakarta包名之外还有两点要留意。一是Spring 6 要求 JDK 17 起步如果你的项目还挂在 JDK 8 上那就先别动。二是部分老的扩展点被废弃或调整比如InstantiationAwareBeanPostProcessor的某些方法签名有变化自定义处理器时最好直接对着 6.x 的源码写别照抄网上的老博客那些大多是 4.x 时代的代码能编译不代表语义正确。我个人在实际操作中的体会是Bean生命周期这东西看十遍图不如自己打一遍日志。把上面那段OrderService完整跑一遍把Transactional加进去再跑一遍把Lazy加上再跑一遍三遍下来你就再也不用背了——因为每次你都能从输出里看见它在哪一步。我到现在也没能完整背出十步法的名字但只要给我一个断点我知道该往哪个方向找。这比背诵有用得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询