直接开聊。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 RedisTemplate<String, String> redisTemplate; @Resource private RedisTemplate<String, 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 Bean:
@Component public class AuthInterceptor implements HandlerInterceptor { @Resource private UserService userService; // ... }然后在配置类里注入这个Bean:
@Configuration 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的。如果你在这里返回null,Spring之后的流程会直接出问题,因为容器拿到的引用就断了。
第二,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注入容器层面的信息。常用的有这些:
| 接口 | 注入的信息 | 典型用途 |
|---|---|---|
BeanNameAware | Bean在容器中的名称 | 需要感知自己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 Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:存放提前暴露的早期单例Bean(半成品) private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 三级缓存:存放ObjectFactory,用于生成早期引用 private final Map<String, 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、application:Web场景下的生命周期变体
request、session、application这三个作用域是Web专属的。它们的生命周期对应了Web请求、会话和应用启动。比如request作用域的Bean,在一次HTTP请求内有效,请求结束就销毁。session作用域类似,在用户会话内有效。
这几个作用域在Spring MVC里用得不少,比如把一个Bean声明为request作用域,让它持有当前用户的请求信息,避免手动从RequestContextHolder里取。但要注意,这些作用域在使用时,要确保当前线程里有对应的Web上下文,否则RequestContextHolder会拿到null,Bean创建失败。
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依赖B,B依赖C,C又依赖A,中间绕了好几层,需要把整条链看全。
我见过一个比较隐蔽的场景:A依赖B,B依赖C,C依赖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 Bean,FeignClient的代理由FeignClientFactoryBean生成,创建的时机是在@Autowired注入发生的时候,而不是应用启动时。所以如果你在@PostConstruct里提前使用FeignClient,可能会触发它的懒加载创建逻辑,有时候会出现意想不到的初始化顺序问题。
RedisTemplate也有类似的情况,它本身是个普通Bean,但它在初始化时会通过afterPropertiesSet()创建内部的一些序列化器。如果某个Bean在RedisTemplate还没完成初始化时就引用了它,就可能取到不完整的RedisTemplate。所以依赖顺序还是那句话:让Spring按照依赖图管理顺序,不要在一个Bean的初始化里依赖另一个正在初始化中的Bean。
6.5 快速定位Bean生命周期问题的排查清单
我把排查思路整理成一张速查表,排障时对着看,能省不少时间:
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 初始化报异常 | 初始化方法内部逻辑出错 | 查看堆栈定位到具体方法,检查配置和依赖 |
| 属性注入为null | Bean不是容器管理,或注入时机不对 | 确认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生命周期,能分享的暂时就是这些。如果你们在实际项目中还遇到过什么和生命周期相关的诡异问题,或者对哪个阶段有不同理解,欢迎一起讨论。这类问题往往越聊越清楚,很多隐藏的知识点也都是在排查中慢慢浮现出来的。