拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Bean生命周期详解:从三级缓存到实战排坑

直接开聊。Spring Bean的生命周期这几乎是所有Java后端工程师绕不开的话题面试要问出问题要排查写框架组件要用。网上讲这个的文章一抓一大把但很多要么是源码流水账要么是八股文式的阶段背诵读完你还是不知道怎么用它解决实际问题。我准备换个讲法不列枯燥的阶段表而是直接从为什么需要理解它切入把生命周期里最关键的几个扩展点、循环依赖对生命周期的影响、作用域如何改写生命周期这些真正影响你写代码的东西讲透最后附上我实际排查过的几个诡异报错。这篇东西适合刚学Spring想建立整体认知的初学者也适合写了好几年业务代码但没系统梳理过Bean生命周期的老手。1. 一张图看懂Spring Bean从诞生到销毁的全过程先别急着记那些生僻接口我们先建立整体画面感。一个Bean在Spring容器里从无到有大致要经过这么几个阶段实例化构造函数/工厂方法 ↓ 属性填充依赖注入 ↓ Aware回调BeanNameAware、BeanFactoryAware、ApplicationContextAware ↓ BeanPostProcessor前置处理postProcessBeforeInitialization ↓ 初始化InitializingBean.afterPropertiesSet / PostConstruct / init-method ↓ BeanPostProcessor后置处理postProcessAfterInitialization ↓ 就绪开始被使用 ↓ 容器关闭 → 销毁DisposableBean.destroy / PreDestroy / destroy-method注意我故意把PostConstruct和InitializingBean、init-method放在同一个阶段里是因为它们本质上都是在初始化这个时机执行的但是执行顺序有讲究。这个后面单独讲。1.1 实例化和属性填充Bean的出生与进食实例化这一步很多人有误解觉得不就是new一个对象吗对也不对。Spring确实是通过构造函数创建Bean实例的但它不是简简单单地调用构造函数就完事而是先拿到BeanDefinition判断是构造器注入还是无参构造再决定怎么创建。如果你用了构造器注入那Spring得先把构造函数所需的所有依赖都准备好才能进行实例化。这也是为什么构造器注入只适合强依赖的场景——它要求依赖必须先存在。实例化完成之后是属性填充也就是我们常说的依赖注入。这里要特别注意Autowired、Resource、Value这些注解的解析都是在属性填充阶段完成的。也就是说当一个Bean的属性填充阶段跑完它的普通字段依赖、配置值、引用的其他Bean基本都已经就位了。如果你在构造函数里直接使用this.someService这种字段大概率拿到的是null原因就在这里——Spring还没走到属性填充这一步。1.2 初始化阶段Bean的成人礼对象new出来了属性也都填充好了但这时候Bean还不是一个完全体。比如你可能需要在依赖全部注入后做一些数据初始化、资源打开、缓存预热、校验配置等工作。这些工作就应该放在初始化阶段做。Spring提供了一堆在初始化阶段做文章的入口我把它们按优先级列一下PostConstruct注解标注的方法InitializingBean接口的afterPropertiesSet()方法Bean(initMethod ...)或XML里配置的init-method网上有很多文章说这几个的执行顺序是固定的但在我的实测中顺序是PostConstruct先执行然后是afterPropertiesSet()最后是initMethod。注意Spring不同版本在细节上可能有调整但大体就是这个顺序。为什么PostConstruct排最前面因为它在JDK里定义CommonAnnotationBeanPostProcessor这个处理器会优先处理它。1.3 销毁阶段Bean的身后事容器关闭的时候Spring会执行Bean的销毁逻辑。同样有三个入口PreDestroy注解标注的方法DisposableBean接口的destroy()方法Bean(destroyMethod ...)或XML里的destroy-method销毁阶段主要用于释放资源比如关闭数据库连接池、关闭线程池、解除注册表项等。我这里踩过一个很典型的坑单例Bean持有线程池如果不在销毁阶段显式关闭线程池应用重启时经常出现线程泄漏的告警。2. 为什么Bean生命周期值得你花时间搞懂——三个扎心场景前面流程看上去不算复杂但真正值钱的是理解这些阶段背后能解决什么问题。我见过太多人在这几个场景栽跟头全是因为对生命周期理解不透。2.1 场景一静态工具类里使用Spring Bean调用时总是报空指针这个场景极其常见。有人写了一个RedisUtils静态工具类然后在里面定义了一个静态的RedisTemplate字段想着通过Autowired给它赋值结果运行时发现字段始终是null。问题出在哪静态字段的注入不是Spring的默认能力。Autowired只能作用于Spring管理的Bean实例上静态字段属于类级别Spring不会帮你自动注入。这时候正确的做法是让RedisUtils成为一个Spring Bean然后在Bean初始化阶段把自身赋给静态字段。比如实现InitializingBean接口Component public class RedisUtils implements InitializingBean { private static RedisTemplateString, String redisTemplate; Resource private RedisTemplateString, String injectedRedisTemplate; Override public void afterPropertiesSet() { redisTemplate injectedRedisTemplate; } public static String get(String key) { return redisTemplate.opsForValue().get(key); } }这个小技巧的本质就是把实例注入转化为静态字段初始化而完成转换的时机就是Bean生命周期中的afterPropertiesSet()阶段。理解了生命周期你才能明白这段代码为什么这么写。2.2 场景二配置类里Bean初始化依赖了另一个Bean的中间状态数据对不上很多人在Configuration配置类里这么写Bean public DataSource dataSource() { DataSource dataSource new HikariDataSource(); // 设置一堆参数 return dataSource; } Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }看起来没问题但如果dataSource()方法里依赖了一个尚未初始化完成的Bean就很容易踩坑。比如你在dataSource()里读取一个配置Bean的属性而这个配置Bean本身也在初始化中那读到的可能就是默认值不是最终值。这就是生命周期理解不够造成的。配置Bean如果实现了InitializingBean那只有等它的afterPropertiesSet()跑完它的属性才真正就位。你在另一个Bean的创建过程里去读它时机上是不可控的。解决办法一是把依赖关系理清楚二是利用DependsOn强制指定顺序三是在ApplicationRunner之类的阶段做后置检查。2.3 场景三拦截器或过滤器里面注入的Service为null这也是老生常谈的问题了。你在Spring MVC里注册了一个HandlerInterceptor然后在里面Autowired了一个UserService结果请求进来的时候发现userService是null。原因并不复杂拦截器实例可能不是Spring Bean而是通过WebMvcConfigurer.addInterceptors()注册时手动new出来的。手动new出来的对象不在Spring容器管理范围内Spring自然不会帮你做依赖注入。这个问题的解法依然是让拦截器本身成为Spring BeanComponent public class AuthInterceptor implements HandlerInterceptor { Resource private UserService userService; // ... }然后在配置类里注入这个BeanConfiguration public class WebConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor).addPathPatterns(/**); } }这里面的核心逻辑还是一样让Bean的创建和依赖注入都交给Spring托管Spring才有机会在生命周期里完成注入动作。反过来说每当你发现一个对象无法使用自动注入时第一反应应该是它到底是不是Spring管理的Bean。3. 核心扩展点拆解BeanPostProcessor与各类Aware接口的实战玩法如果说Bean生命周期是一台精密仪器那BeanPostProcessor就是你在仪器上装的各种传感器和阀门。Spring的AOP、代理、注解处理统统依赖它。3.1 BeanPostProcessor每个Bean初始化前后的必经检查站先看一个简单示例Component public class LogBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof UserService) { System.out.println([BEFORE] beanName 即将初始化); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof UserService) { System.out.println([AFTER] beanName 初始化完成); } return bean; } }注意两个重点。第一postProcessAfterInitialization的返回值非常关键。如果你想对某个Bean做代理增强比如Transactional、Async的代理生成就是在这个方法里把原始Bean替换成代理Bean的。如果你在这里返回nullSpring之后的流程会直接出问题因为容器拿到的引用就断了。第二BeanPostProcessor是针对容器内所有Bean的不是单个Bean。所以写的时候一定要做好类型过滤否则日志量会爆炸性能也会受影响。3.2 用BeanPostProcessor模拟Spring AOP的代理增强过程理解了BeanPostProcessor你其实就理解了Spring AOP的动态代理是从哪里介入的。我写个简化版的代理逻辑给你看Component public class LogAspectBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof OrderService) { Object proxyBean Proxy.newProxyInstance( bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), (proxy, method, args) - { System.out.println([AOP] 方法执行前: method.getName()); Object result method.invoke(bean, args); System.out.println([AOP] 方法执行后: method.getName()); return result; } ); return proxyBean; } return bean; } }这段代码虽然简陋但和Spring AOP在生命周期中做代理的时机是一致的目标Bean已经完成初始化然后在postProcessAfterInitialization阶段被包装成代理对象。代理对象替换了原始对象后续所有通过容器获取的Bean拿到的都是这个代理。这解释了另一个经典问题为什么this调用同一个类中的Transactional方法时事务不生效因为事务代理只对从容器中取出的引用有效this指向的是原始对象不是代理对象方法调用直接走原始逻辑事务没有介入的机会。3.3 Aware接口全家桶让Bean知道自己在容器里的身份Aware接口是一组以Aware结尾的接口作用是给Bean注入容器层面的信息。常用的有这些接口注入的信息典型用途BeanNameAwareBean在容器中的名称需要感知自己beanName的场景BeanFactoryAware所属的BeanFactory需要动态获取其他Bean的工厂场景ApplicationContextAware容器上下文获取Spring容器中的各种能力EnvironmentAware环境配置读取环境变量、配置文件的场景ResourceLoaderAware资源加载器加载classpath下资源的场景这里我说一下实际经验。ApplicationContextAware是使用率最高的一个很多人喜欢在工具类里通过它获取ApplicationContext然后实现一个SpringContextHolder。但要注意ApplicationContextAware回调发生在Bean初始化之前的Aware阶段所以不要在实现类的主构造函数中直接调用SpringContextHolder.getBean()因为容器可能还没调用Aware回调此时工具类里的applicationContext字段还是null。3.4 初始化三种写法的选型建议PostConstruct、InitializingBean、init-method都能在初始化阶段干活那我该用哪个我的建议是业务代码优先用PostConstruct因为它是Java标准注解不依赖Spring特有接口将来如果脱离Spring环境代码可迁移性更强。框架代码或者通用组件可以考虑InitializingBean因为它是Spring原生机制语义明确处理顺序上更可控。init-method适合在配置类里给第三方库的Bean做初始化动作不需要改第三方代码。4. 三级缓存与循环依赖生命周期中隐藏的作弊机制既然聊生命周期就绕不开循环依赖这个话题。热搜词里也有spring三级缓存原理这确实是Spring生命周期中最让人迷惑的设计之一。4.1 为什么循环依赖会和生命周期冲突先看一个典型的循环依赖场景Service public class UserService { Resource private OrderService orderService; } Service public class OrderService { Resource private UserService userService; }如果没有特殊机制这个场景会直接导致创建失败。原因顺着生命周期思考就清楚了。当Spring创建UserService时先实例化对象然后进入属性填充阶段发现需要OrderService于是转而去创建OrderService。OrderService实例化后进入属性填充发现需要UserService于是又转回去创建UserService。但是此刻UserService还在创建过程中没有走到postProcessAfterInitialization严格来说它还不能算一个完整的Bean。如果Spring死板地等UserService完全初始化那双方就僵住了永远等不到对方就绪形成死锁。Spring的解决办法是提前暴露半成品对象。也就是在UserService实例化完成、尚未完成属性填充和初始化的时候先把这个半成品引用存入一个缓存让后续需要它的Bean可以先拿着这个引用顶着等UserService彻底初始化完成后再去补充完整能力。这个半成品缓存的机制就是三级缓存的核心逻辑。4.2 三级缓存到底是什么直接看代码// 一级缓存存放已经完全创建好的单例Bean private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存存放提前暴露的早期单例Bean半成品 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存存放ObjectFactory用于生成早期引用 private final MapString, ObjectFactory? singletonFactories new HashMap(16);三级缓存的设计逻辑是这样的一级缓存是最终状态所有创建完的Bean都在这里。三级缓存存的是ObjectFactory工厂对象这个工厂可以在需要的时候创建出早期引用。之所以不直接存对象是为了在生成早期引用时还能有机会插入AOP代理。也就是说如果这个Bean最终需要被代理那么提前暴露的应该是代理对象而不是原始对象。二级缓存是三级缓存的成品化保存处。ObjectFactory.getObject()执行完之后得到早期引用把这个引用放入二级缓存同时移除三级缓存中的工厂对象。4.3 三级缓存解析的具体流程我尽可能用容易理解的方式描述一遍。UserService开始创建实例化完成之后Spring把它包装成ObjectFactory放入三级缓存。然后进入属性填充阶段发现依赖OrderService于是先尝试从一级缓存获取没拿到再从二级缓存获取也没有最后从三级缓存获取拿到的是ObjectFactory。执行ObjectFactory.getObject()得到UserService的早期引用放入二级缓存同时移除三级缓存条目。这时创建OrderService时将早期引用注入进去即可。OrderService创建完成后UserService继续执行属性填充、初始化最终创建完成后放入一级缓存。此时二级缓存里的早期引用和一级缓存里的完整引用是同一个对象吗不一定。如果UserService在postProcessAfterInitialization阶段被替换成了代理对象那早期引用和最终引用就不是同一个对象了。这也是AOP和循环依赖叠加时会出现的经典坑。4.4 一个经典坑AOP代理对象的枪口对准谁如果UserService没有AOP增强那么提前暴露的早期引用和最终创建的Bean是同一个对象一切风平浪静。如果UserService需要AOP代理事情就变得微妙了。因为提前暴露的对象是在AOP代理生成之前就创建的OrderService持有的UserService引用是原始对象。而容器最终放入一级缓存的UserService是代理对象。这会导致什么后果如果OrderService里调用userService.someMethod()走的是原始对象的方法事务、日志等AOP功能全部失效。但Spring其实已经考虑过这个问题。它的处理方式是在getEarlyBeanReference()阶段就通过SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference()方法提前生成代理对象保证提前暴露的引用就是代理对象。也就是说只要循环依赖调整顺序正确AOP代理在提前暴露时就已经完成了后面postProcessAfterInitialization阶段会保持同一个代理实例不再重复创建。这就能解释为什么有人会发现在某些循环依赖场景下Transactional代理的创建时机和其他场景不一样。本质上都是为了拿到同一个代理引用而做的特殊处理。4.5 哪些循环依赖Spring解决不了三级缓存是个复杂的补偿机制但它不是万能的。以下情况容器会直接抛BeanCurrentlyInCreationException构造器注入的循环依赖。因为构造器注入要求在实例化之前就准备好依赖这时候不可能提前暴露半成品毕竟连对象都还没new出来。Async注解的Bean循环依赖。异步代理的创建时机比较特殊普遍不建议在Async相关的Bean之间互相依赖。设置了DependsOn强制依赖关系的Bean循环依赖。单例之外的其他作用域循环依赖比如prototype因为prototype作用域的Bean不缓存根本无从提前暴露。我个人的习惯是尽量避免循环依赖。它不是必须解决的deadlock更多时候是一种设计上的坏味道。循环依赖的Bean往往意味着职责边界不清晰。与其依赖三级缓存的精巧机制不如把循环依赖的依赖关系抽出来放到第三方的组件里统一维护。5. Bean的作用域如何改写Bean的生命周期剧本生命周期不是一套固定不变的剧本它受作用域影响很大。不同作用域下Bean的创建次数、缓存方式、销毁时机都完全不同。5.1 singleton默认的一次一生singleton作用域下Spring容器只会创建一个Bean实例。这个实例在一级缓存中存活直到容器关闭时统一销毁。注意singleton不等于享元模式但思想接近。因为它被所有调用方共享所以如果你在单例Bean里定义了可变状态字段并发访问时就会有线程安全问题。我之前遇到过一个实际问题一个单例服务里维护了一个Map做本地缓存没有做并发控制结果线上出现偶发数据错乱。排查半天发现是HashMap并发写入导致的。解决方式要么用ConcurrentHashMap要么加锁要么把缓存拆到专门的组件里。singleton作用域Bean的销毁时机也比较晚它是容器关闭时批量处理的。如果单个Bean持有昂贵的资源比如数据库连接池等待容器关闭时统一清理可能太晚尤其是应用在运行期间反复创建和销毁容器的场景。不过这种情况比较少见。5.2 prototype每次getBean都是从头走一遍prototype作用域下每次从容器获取Bean都会创建一个新实例。也就是说完整生命周期中实例化→属性填充→初始化会反复执行。但注意Spring对prototype作用域的Bean有一个明显的区别容器只负责创建和初始化不负责销毁。这句话怎么理解Spring容器在关闭时只会对singleton作用域的Bean调用销毁回调prototype的Bean则被放养因为容器不知道你到底持有了多少个它的实例也无从跟踪清理。这带来一个很实际的坑如果prototypeBean实现了DisposableBean或配置了destroy-method那你不能指望容器关闭时帮你释放资源。你需要自己在使用完Bean之后手动清理或者借助Scope(prototype) 自定义后置处理器的组合来管理。5.3 request、session、applicationWeb场景下的生命周期变体request、session、application这三个作用域是Web专属的。它们的生命周期对应了Web请求、会话和应用启动。比如request作用域的Bean在一次HTTP请求内有效请求结束就销毁。session作用域类似在用户会话内有效。这几个作用域在Spring MVC里用得不少比如把一个Bean声明为request作用域让它持有当前用户的请求信息避免手动从RequestContextHolder里取。但要注意这些作用域在使用时要确保当前线程里有对应的Web上下文否则RequestContextHolder会拿到nullBean创建失败。5.4 作用域选型和生命周期管理建议我的建议是默认全部用singleton因为它性能最好、损耗最小、也最容易排查问题。确实需要不同状态隔离的场景优先考虑把状态放到方法级或者使用ThreadLocal而不是直接改成prototype。prototype会带来更高的创建开销和更复杂的资源回收问题。只有当你明确需要每次都获得一个全新独立实例且资源开销可以承受时才选择prototype。6. 实战排查那些年我们一起踩过的Bean生命周期坑理论讲再多不如实战来得实在。这一节我整理几个我之前实际遇到和排查过的经典问题附带完整的排查思路。因为我本身踩过的坑也不少很多问题排查到最后回溯到根因时发现就是生命周期某一环出了问题。6.1 报错Error creating bean with name xxx: Invocation of init method failed这个报错应该是Spring开发者最常见的报错之一了。字面意思是在调用Bean的初始化方法时抛出了异常。排查思路很简单。先看堆栈确定是哪个Bean的哪个初始化方法报错如果是PostConstruct方法报错说明初始化逻辑失败比如配置数据没准备好或者依赖的资源暂不可用。如果是afterPropertiesSet()报错通常是校验逻辑不满足比如某个必填配置项为空。如果是init-method报错可能是第三方组件初始化失败。这类问题九成是初始化逻辑本身的问题建议在初始化方法里做好防御式校验把关键参数打印出来避免异常信息过于模糊。另外不要在PostConstruct里做长时间阻塞操作比如远程调用因为Bean初始化是在容器启动阶段完成的一个Bean卡住整个应用就起不来。我之前见过有人在PostConstruct里调外部接口做数据同步结果外部接口响应超时整个服务启动被拖了十几分钟。后来改成了异步监听ApplicationReadyEvent再做这些操作启动速度快了很多。6.2 循环依赖报错BeanCurrentlyInCreationException这个报错出现的时候完整堆栈里通常会附带类似Requested bean is currently in creation: Is there an unresolvable circular reference?的信息。排查方式也不复杂。第一步根据堆栈定位哪些Bean存在相互引用。第二步看它们是通过构造器注入还是setter/字段注入。如果是构造器注入那结论基本明确Spring解决不了这种场景需要手动重构。第三步如果既不是构造器注入代码里也没有明显的依赖环那多半是间接循环依赖比如A依赖BB依赖CC又依赖A中间绕了好几层需要把整条链看全。我见过一个比较隐蔽的场景A依赖BB依赖CC依赖A但A和C之间的依赖是通过Lazy注解标注的。Lazy在注入时生成一个代理对象调用时才真正解析依赖所以Spring可以借此打破循环。这种解法虽然有效但是会让排查链路变得更复杂不建议大量使用。更推荐的做法是拆掉循环依赖本身。6.3 后台日志频繁打印BeanNameAware/BeanFactoryAware not called或者Aware注入的字段为null这个问题遇到过的人应该不少。当你实现了一个Aware接口结果字段还是null第一反应不要怀疑Spring坏了先检查这个Bean到底是不是容器管理的。比如你通过new UserService()这样的方式创建了Bean那它肯定不在容器管理范围内Aware回调和依赖注入都不会生效。还有一种情况是Bean虽然声明了但被Configuration里的某个Bean方法手动覆盖了容器里最终存的不是你定义的那个实例。排查时可以打一下启动日志确定注册进容器的到底是谁。6.4 FeignClient、RedisTemplate这类框架组件的生命周期特殊性如果你用Spring Cloud应该知道FeignClient是通过feign的构建器在运行时创建的。它的生命周期其实不完全等同于普通Spring BeanFeignClient的代理由FeignClientFactoryBean生成创建的时机是在Autowired注入发生的时候而不是应用启动时。所以如果你在PostConstruct里提前使用FeignClient可能会触发它的懒加载创建逻辑有时候会出现意想不到的初始化顺序问题。RedisTemplate也有类似的情况它本身是个普通Bean但它在初始化时会通过afterPropertiesSet()创建内部的一些序列化器。如果某个Bean在RedisTemplate还没完成初始化时就引用了它就可能取到不完整的RedisTemplate。所以依赖顺序还是那句话让Spring按照依赖图管理顺序不要在一个Bean的初始化里依赖另一个正在初始化中的Bean。6.5 快速定位Bean生命周期问题的排查清单我把排查思路整理成一张速查表排障时对着看能省不少时间症状可能原因排查方向初始化报异常初始化方法内部逻辑出错查看堆栈定位到具体方法检查配置和依赖属性注入为nullBean不是容器管理或注入时机不对确认Bean是否由Spring创建检查字段是否被static修饰循环依赖创建失败构造器注入循环或prototype循环定位依赖链重构依赖关系静态工具类无法注入静态字段不支持自动注入通过InitializingBean或PostConstruct赋值事务/异步不生效this调用内部方法代理未介入从容器获取代理对象或拆类prototype Bean资源未释放容器不管理prototype销毁手动清理或使用自定义销毁逻辑这张表是我日常排障时常用到的基本能覆盖绝大多数生命周期相关的坑。6.6 安全相关的一个小提醒关于Spring框架本身的安全性也是个老话题了。热搜词里出现了类似Spring Framework目录遍历漏洞的条目虽然说的是CVE-2024-38819这类已披露的旧漏洞但我每次看到还是要啰嗦一句框架版本一定要及时升级。就Spring框架而言版本滞后是很多已知漏洞能够被利用的直接原因。Bean生命周期的讨论再深入也顶不住一个不打补丁的底层框架被别人攻破。生命周期相关代码里经常会打开文件资源、网络连接这些地方尤其要小心路径穿越和资源泄漏问题别把用户输入直接拼接到文件路径里。7. 生命周期源码阅读建议从哪里入手最快我不是很喜欢一上来就扔几百行源码但对生命周期来说如果你能从源码层面验证一遍自己理解的流程会记得非常牢。给你一条我自己觉得效率最高的阅读路径。7.1 从AbstractApplicationContext.refresh()切入Spring容器的启动核心是refresh()方法。它是了解全球流程的总入口生命周期相关的扫描、注册、创建都从这里展开。你不需要逐行看完重点是看finishBeanFactoryInitialization()这一行它负责实例化所有非懒加载的单例Bean。点进去你会看到preInstantiateSingletons()再往深层走就是getBean()。7.2 核心方法链路追踪想真正看清生命周期的那几步核心链路大概是这样AbstractBeanFactory.doGetBean() → getSingleton(beanName, () - createBean(...)) → AbstractAutowireCapableBeanFactory.createBean() → doCreateBean() → createBeanInstance() // 实例化 → populateBean() // 属性填充 → initializeBean() // 初始化 → invokeAwareMethods() // Aware回调 → applyBeanPostProcessorsBeforeInitialization() → invokeInitMethods() // 初始化方法 → applyBeanPostProcessorsAfterInitialization()建议不要顺着源码死盯而是断点打在关键方法上运行一个极简的测试工程看每个阶段的执行顺序和参数变化。我看到很多人说源码枯燥其实是方法不对。源码这东西最好是带着问题去读比如你就带着为什么字段注入为null这个问题去读populateBean()会理解得特别快。7.3 动手验证生命周期最直观的方式实践是最好的理解方式。你可以写一个简单的验证工程定义一个UserService让它实现BeanNameAware、BeanFactoryAware、InitializingBean、DisposableBean同时在Bean配置上指定initMethod和destroyMethod再注册一个BeanPostProcessor在前后置处理里打印日志。启动容器时观察日志顺序关闭容器时再观察销毁顺序。这样跑一遍实际看到的东西远比背十遍八股文有用。而且你还会发现很多文档里没有细讲的细节比如PostConstruct和InitializingBean到底谁先执行配置了destroyMethod和DisposableBean.destroy()时谁会先跑。8. 关于Bean生命周期我最后想分享的一些经验聊了这么多最后分享几条我自己在实战中的体会也是我教团队新人时反复强调的重点。第一处理生命周期相关问题时先判断这个对象到底是不是Spring容器管理的Bean。很多灵异问题比如注入为null、Aware不生效、事务不生效根因都是这里。你把这个前提确认了至少一半的坑可以避开。第二BeanPostProcessor是Spring最强大的扩展点之一但它也是双刃剑。因为它对容器内所有Bean生效一旦逻辑处理不当性能损耗是全局的。写这类组件时一定要做精确的类型匹配并且保证处理逻辑足够轻量不要在里面做重IO操作。第三初始化阶段不是万能收容所。很多操作其实不适合在PostConstruct或afterPropertiesSet()里做。比如依赖外部系统的数据同步、耗时较长的预热逻辑这些放到ApplicationRunner和ApplicationReadyEvent阶段执行更合理。前者是Bean准备阶段后者是应用启动完成后区分清楚能让启动速度更快也更不容易被外部依赖拖垮。第四对PostConstruct和PreDestroy保持偏爱。它们是JDK标准注解不依赖Spring特有接口代码可迁移性更好。如果你写的模块未来有可能脱离Spring容器运行用标准注解会比用Spring原生的InitializingBean更安全。第五循环依赖的顶层设计方案一定要以消除为目标。三级缓存的机制再精妙也只是妥协方案。它增加了理解和排查的复杂度也带来了一些和AOP代理相关的边界问题。能通过拆分对象、引入中间层来消除循环依赖的话永远比依赖框架机制去兜底更可靠。关于Spring Bean生命周期能分享的暂时就是这些。如果你们在实际项目中还遇到过什么和生命周期相关的诡异问题或者对哪个阶段有不同理解欢迎一起讨论。这类问题往往越聊越清楚很多隐藏的知识点也都是在排查中慢慢浮现出来的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门