干这行这么多年,Spring Bean生命周期是被问得最多、也最容易讲混的一道题。刚工作那会儿背过八股,后来手写迷你Spring、排查线上循环依赖问题、给同事讲解三级缓存原理,才逐渐把这些阶段真正串成一条线。这篇就把我理解的完整生命周期、源码层面的关键节点,以及面试和实战里常遇到的坑一并讲清楚。
Spring Bean生命周期不是说new一个对象那么简单,它涵盖了从配置解析、实例化、属性填充、初始化到最终销毁的全过程。理解它,你才能真正看懂Spring容器做了什么、BeanPostProcessor在哪一步介入、循环依赖为什么能解决、AOP代理又是在哪个环节生效的。无论你是刚入门的Java开发者,还是准备高级岗位面试的选手,这部分内容都值得花时间彻底吃透。
1. 生命周期全景图谱:一道面试题背后的大厦
1.1 为什么Bean生命周期是理解Spring的钥匙
很多同学学Spring第一件事就是写一个Controller,配一个Service,然后注解一加就跑起来了。但一旦遇到问题就抓瞎:为什么@Autowired能注入成功?为什么AOP切面有时候生效有时候不生效?为什么循环依赖在Spring Boot里默认能跑起来?
这些问题的答案,全都在Bean生命周期里。
我习惯把Spring容器比作一家餐厅的后厨。你只需要点菜(声明一个Bean),后面的事情——买菜(解析配置)、洗菜切菜(实例化)、配菜装盘(属性填充)、上灶烹饪(初始化)、营业结束打烊(销毁)——全部由后厨流水线完成。后厨有一个个小工(BeanPostProcessor),在上菜前后可以偷偷给菜品加料(增强逻辑、生成代理)。
搞懂这条流水线,你就掌握了Spring框架里90%的“魔法”。尤其是后厨里小工(BeanPostProcessor)干活的位置,决定了AOP、事务、异步注解这些东西能不能生效。
1.2 全景流程:从配置到容器销毁的八个阶段
先看完整路径,这八个阶段是我自己梳理的,背下来后面就好理解了。
- BeanDefinition解析:Spring读取XML配置、注解或Java Config,生成BeanDefinition元数据。
- 实例化(Instantiation):通过构造器反射创建原始对象,此时对象的所有属性都是默认值。
- 属性填充(Populate):根据依赖注入配置,填充基本类型属性、对象引用(@Autowired、@Resource)、集合等。
- Aware回调:如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口,在此阶段回调,把容器信息告诉Bean。
- BeanPostProcessor前置处理(postProcessBeforeInitialization):在初始化前的钩子,Spring自身的ApplicationContextAwareProcessor、AutowiredAnnotationBeanPostProcessor都在这里工作。
- 初始化(Initialization):依次执行@PostConstruct方法、InitializingBean接口的afterPropertiesSet方法、自定义init-method。
- BeanPostProcessor后置处理(postProcessAfterInitialization):初始化后的钩子,AOP创建代理对象的关键位置就在这里。
- 使用与销毁(Destruction):容器关闭时,依次执行@PreDestroy方法、DisposableBean接口的destroy方法、自定义destroy-method。
注意,步骤7是个分水岭。很多同学理解的“Bean”其实是代理对象,真实Bean可能在步骤7就被藏起来了。
下面给一个完整速查表,接口/注解和对应阶段一目了然:
| 机制 | 执行阶段 | 典型用途 |
|---|---|---|
| 构造器 | 实例化 | 创建原始对象,建议注入必需依赖 |
| @Autowired / setter | 属性填充 | 依赖注入 |
| BeanNameAware | 阶段4 | 获取BeanName |
| BeanFactoryAware | 阶段4 | 获取BeanFactory |
| ApplicationContextAware | 阶段4 | 获取ApplicationContext |
| postProcessBeforeInitialization | 阶段5 | 修改Bean属性、包装Bean |
| @PostConstruct | 阶段6 | 初始化资源、校验配置 |
| afterPropertiesSet | 阶段6 | 初始化逻辑(Spring原生接口) |
| init-method | 阶段6 | XML时代的初始化方法 |
| postProcessAfterInitialization | 阶段7 | AOP代理、生成代理Bean |
| @PreDestroy | 阶段8 | 释放资源、关闭连接 |
| destroy | 阶段8 | 销毁逻辑(DisposableBean) |
| destroy-method | 阶段8 | 自定义销毁方法 |
1.3 为什么单例Bean和原型Bean生命周期不一样
Bean作用域不同,生命周期管理的深度也不同。
单例(singleton)Bean的完整生命周期由Spring容器管理:从实例化到销毁全流程都走完,容器关闭时会触发销毁回调。这也是最常见的Bean形态。
原型(prototype)Bean就要特别注意:创建后容器会交付给调用方,容器只负责“生”不负责“养”。初始化回调(@PostConstruct等)会执行,但销毁回调(@PreDestroy等)根本不会自动执行。什么时候释放资源,完全取决于使用方自己。
我在项目里吃过这个亏:把某个耗时任务Bean设为prototype,里面用到了线程池,结果容器销毁后线程池一直没关闭,导致应用多次重启后线程泄露。后来改成手动调用销毁方法才解决。这个问题在面试里也经常被问,记住一句话:prototype作用域下,Spring只管生不管死。
2. 核心阶段源码级拆解:每个节点背后发生了什么
2.1 实例化:Spring怎么把一个类变成对象
实例化是整个生命周期的起点。Spring默认通过无参构造器或指定构造器来创建对象,底层是反射调用。
正常路径下,AbstractAutowireCapableBeanFactory的createBeanInstance方法会做三件事:
- 检查BeanDefinition是否指定了Supplier或工厂方法
- 通过构造器解析(ConstructorResolver)匹配最合适的构造器
- 反射实例化
这里面有个重要参数:@Autowired标注在有参构造器上时,Spring会自动选择该构造器进行依赖注入。Spring框架5.x之后,如果Bean只有一个构造器,即使不标注@Autowired也会被自动使用。我见过老项目里一个人写了三四个构造器,结果Spring不知道该选哪个,直接报错。解决办法很明确:要么保留无参构造器,要么明确标注一个主构造器。
实例化阶段有个容易被忽略的细节:实例化返回的原始对象此时还是“裸”的,属性全是默认值。如果你想在实例化后、属性填充前做点事情,可以实现InstantiationAwareBeanPostProcessor接口,在postProcessBeforeInstantiation方法中返回自定义实例。Spring自身用这个扩展点实现了AOP的提前代理(AbstractAutoProxyCreator就在这一步检查是否需要为当前Bean创建代理),这也是三级缓存能解决循环依赖的关键前置条件。
2.2 属性填充与Aware回调:Bean如何“组装”起来
实例化之后进入populateBean阶段,这是依赖注入真正发生的地方。
Spring会先处理InstantiationAwareBeanPostProcessor的postProcessProperties方法,AutowiredAnnotationBeanPostProcessor利用这个方法扫描@Autowired和@Value注解,然后逐个解析依赖并注入。所以在面试里我说过一句话:@Autowired的本质是一个BeanPostProcessor在属性填充阶段干的活,推而广之,@Resource是CommonAnnotationBeanPostProcessor干的活,两者底层套路一致,都是“后置处理器扫描注解、解析依赖、强制注入”。
依赖注入在属性填充阶段完成后,Spring进入Aware回调阶段。这里多个Aware接口的调用顺序是固定的:
BeanNameAware:回调setBeanName,传入Bean的id/name。BeanClassLoaderAware:回调setBeanClassLoader,传入加载该Bean的ClassLoader。BeanFactoryAware:回调setBeanFactory,传入BeanFactory实例,用于后续手动获取其他Bean。ApplicationContextAware:这个不是由BeanFactory直接回调的,而是通过ApplicationContextAwareProcessor这个BeanPostProcessor在postProcessBeforeInitialization阶段回调setApplicationContext。
每次面试我都建议候选人把这些Aware接口的触发时机背熟,尤其是ApplicationContextAware的触发位置。它不在属性填充阶段,而在初始化前的前置处理里。这解释了为什么在setter注入时拿不到ApplicationContext,但在@PostConstruct里可以。
2.3 BeanPostProcessor:Spring最强大的扩展点
如果说Bean生命周期是一条流水线,BeanPostProcessor就是可以在流水线上自由布置的检查站。
接口定义两个方法:
public interface BeanPostProcessor { default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }这两个方法分别作用于初始化方法(@PostConstruct、afterPropertiesSet等)的前后。注意返回值:你可以返回包装后的代理对象,也可以返回原对象,甚至可以偷天换日返回一个完全不同的对象——只要类型兼容。
我自己写过一个需求:给订单服务中所有接口方法打日志,但不想侵入业务代码。最初方案是用AOP,后来发现某个遗留Bean是XML配置的老接口,直接实现BeanPostProcessor,在postProcessAfterInitialization阶段判断Bean类型,然后用JDK动态代理包一层,写日志、记录耗时、做降级开关。这么干完全绕开了AOP对类加载顺序、切面配置的依赖,效果还特别干净。
Spring内部大量核心功能都是这样钩上来的:
- AutowiredAnnotationBeanPostProcessor:处理@Autowired
- CommonAnnotationBeanPostProcessor:处理@Resource、@PostConstruct、@PreDestroy
- AbstractAutoProxyCreator:创建AOP代理,支撑@Transactional、@Async
- ApplicationContextAwareProcessor:处理ApplicationContextAware回调
所以“如何向Spring容器中动态注入Bean”“如何给Bean动态生成代理”,本质都是自定义BeanPostProcessor。
2.4 初始化三兄弟:@PostConstruct、InitializingBean、init-method
初始化阶段同一时间点有三个执行入口,顺序是固定不变的:
- 先执行
@PostConstruct注解方法 - 再执行
InitializingBean接口的afterPropertiesSet() - 最后执行XML配置或
@Bean(initMethod = "...")指定的init-method
用一段验证代码:
@Component public class LifeCycleBean { public LifeCycleBean() { System.out.println("1. 构造方法"); } @PostConstruct public void postConstruct() { System.out.println("2. @PostConstruct"); } @Override public void afterPropertiesSet() { System.out.println("3. InitializingBean.afterPropertiesSet"); } public void customInit() { System.out.println("4. init-method"); } }配合@Bean(initMethod = "customInit")注册,运行后输出顺序固定为1234。这个顺序在源码里由initializeBean方法保证:
protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) { // 先执行Aware回调 invokeAwareMethods(beanName, bean); // 前置处理:@PostConstruct所在阶段 Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); // 初始化:afterPropertiesSet + init-method invokeInitMethods(beanName, wrappedBean, mbd); // 后置处理:AOP代理 wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); return wrappedBean; }看到源码就明白为什么@PostConstruct总是先执行:它是靠CommonAnnotationBeanPostProcessor在postProcessBeforeInitialization里触发的,天然排在afterPropertiesSet之前。
实践建议:项目里统一用一种初始化方式。我主导的团队规范里约定:业务代码用@PostConstruct,框架性、组件性的代码用InitializingBean。init-method只用于XML配置遗留系统,新代码不写,能在代码里显式看到生命周期钩子的地方尽量不用XML魔法。
2.5 销毁阶段:资源释放的最后一道门
容器关闭时,Spring会遍历所有单例Bean,找到SmartInitializingSingleton和DisposableBean进行处理,执行顺序同样固定:
- 先执行
@PreDestroy标注的方法 - 再执行
DisposableBean的destroy()方法 - 最后执行配置的
destroy-method
@Component public class LifeCycleBean { @PreDestroy public void preDestroy() { System.out.println("1. @PreDestroy"); } @Override public void destroy() { System.out.println("2. DisposableBean.destroy"); } public void customDestroy() { System.out.println("3. destroy-method"); } }日常开发常见的一个错:把数据库连接池、线程池放在@PreDestroy里关闭,看似合理,但在某些优雅停机场景下,Spring Cloud的注册中心摘除和Bean销毁的先后顺序会引发短时间调用失败。我们的做法是:在@PreDestroy里先切流量,再关闭连接池;或者实现SmartLifecycle接口,通过getPhase控制停机阶段的优先级。
销毁阶段还有一个容器的行为值得注意:单例Bean默认在容器关闭时统一销毁,但prototypeBean不会。有人问那prototype Bean的资源怎么释放?两个办法:一是自己维护引用列表,在容器关闭钩子里手动清理;二是用一个专门的“消费者”单例Bean管理原型Bean的销毁。没有第三种自动方案。
3. 三级缓存:生命周期里最容易被神话的原理
3.1 为什么需要三级缓存
Spring的循环依赖,指的是A依赖B、B依赖A。在属性填充阶段,A需要B,B需要一个初始化的A,但A还没初始化完——正常的生命周期走不通。
我先把结论说清楚:三级缓存解决的是单例Bean在属性填充阶段发生循环依赖的问题,它不解决构造器注入的循环依赖,也不解决prototype Bean的循环依赖。
三级缓存是三个Map,名字和用途很有讲究:
/** 一级缓存:完整的成品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还没填充属性,Spring会把一个
ObjectFactory放进三级缓存 - 发生循环依赖时,通过三级缓存的工厂方法提前拿到一个“早期引用”,放入二级缓存
- 初始化完成,放入一级缓存,同时清理二、三级缓存
3.2 三级缓存解决循环依赖的完整时序
假设A和B互相依赖,过程大致这样:
- Spring开始创建A,反射实例化A得到原始对象a。
- 提前把a包装成
ObjectFactory放入三级缓存。 - 开始给a填充属性,发现依赖B。
- 去容器里找B,发现B没有创建,于是开始创建B。
- Spring实例化B,把B的ObjectFactory放入三级缓存。
- 给B填充属性,发现依赖A。
- 查找A:三级缓存里有A的ObjectFactory,调用getEarlyBeanReference得到一个“早期引用”。此时如果A需要AOP,这个早期引用可能就是代理对象,但A的初始化方法(@PostConstruct等)还没执行。
- B拿到早期A,顺利完成属性填充,初始化完成,B被放入一级缓存。
- 回到A的创建流程,A拿到已经创建好的B,继续填充属性,完成初始化。
- A最终完成后放入一级缓存。
整个过程里,A并不是“完整生命周期走完”后才被B引用,而是处于“半成品”状态就被分享出去了。这正是三级缓存的本质:把Bean的可用性(可被引用)和完整性(已完成初始化)解耦。
3.3 从缓存看生命周期的两个延伸结论
第一个结论:为什么构造器注入无法解决循环依赖。构造器注入要求A在实例化时就把B准备好,但B实例化时又需要A的构造器参数,此时A连“半成品”状态都不存在,三级缓存里根本找不到可以提前暴露的对象,死锁直接报错。我会在项目规范里明确:核心Bean禁止构造器循环依赖,一旦出现就要重构。
第二个结论:AOP代理在三级缓存阶段就参与进来了。第三步提到getEarlyBeanReference,这个方法会检查Bean是否需要增强(是否有Advisor匹配),需要就提前创建代理对象。结合BeanPostProcessor第7步postProcessAfterInitialization也会创建代理,你可能会问:会不会重复代理?不会,Spring有earlyProxyReferences机制,通过一个Map记录哪些Bean已经被提前代理,后置阶段就跳过。
这解释了面试高频问题:循环依赖情况下,为什么注入到B里的A已经是代理对象?因为A在三级缓存阶段通过ObjectFactory提前完成了代理创建,B拿到手里就是增强后的对象。
4. 手写一个迷你Spring:自己动手理解生命周期全过程
4.1 核心设计思路
背再多理论不如动手写一遍。我曾在技术分享时带着团队用100多行代码实现过一个只支持@Component扫描和@Autowired的迷你Spring,核心就是模拟Bean生命周期的各个阶段。这段经验让我真正理解了Spring设计者的意图。
设计方向:
- 扫描指定包下所有类,解析带
@Component注解的类,注册BeanDefinition - 按生命周期顺序处理:实例化、属性填充、Aware回调、初始化、后置处理
- 维护一个极简三级缓存,演示循环依赖解决
- 定义一个BeanPostProcessor接口,验证它在生命周期中的位置
这个迷你框架虽然简陋,但跑起来后能清晰打印出每个Bean在每个阶段的调用日志。
4.2 核心代码实现
自定义注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MiniComponent { } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MiniAutowired { } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MiniPostConstruct { }生命周期核心容器:
public class MiniApplicationContext { private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(); private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(); private final List<BeanPostProcessor> postProcessors = new ArrayList<>(); public MiniApplicationContext(Class<?> configClass) { // 1. 扫描包路径,注册BeanDefinition scan(configClass.getPackageName()); // 2. 预实例化所有单例Bean(模拟Spring的preInstantiateSingletons) singletonNames.forEach(this::getBean); // 3. 注册容器关闭钩子 } public Object getBean(String beanName) { Object bean = singletonObjects.get(beanName); if (bean != null) { return bean; } // 二级缓存命中:早期引用 bean = earlySingletonObjects.get(beanName); if (bean != null) { return bean; } // 三级缓存:生成早期引用 ObjectFactory<?> factory = singletonFactories.get(beanName); if (factory != null) { bean = factory.getObject(); earlySingletonObjects.put(beanName, bean); singletonFactories.remove(beanName); return bean; } return doCreateBean(beanName); } private Object doCreateBean(String beanName) { // 阶段1: 实例化 Object bean = instantiate(beanName); // 阶段2: 提前暴露到三级缓存,解决循环依赖 singletonFactories.put(beanName, () -> { // 这里会调用BeanPostProcessor实现AOP代理逻辑 return applyPostProcessorsBeforeInit(bean, beanName); }); // 阶段3: 属性填充(模拟@Autowired) populateBean(bean, beanName); // 阶段4: 初始化(@PostConstruct) invokeInitMethods(bean, beanName); // 阶段5: 完整Bean放入一级缓存 singletonObjects.put(beanName, bean); earlySingletonObjects.remove(beanName); singletonFactories.remove(beanName); return bean; } private void populateBean(Object bean, String beanName) { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(MiniAutowired.class)) { field.setAccessible(true); String dependName = field.getName(); Object dependBean = getBean(dependName); // 触发循环依赖解析 field.set(bean, dependBean); } } } private void invokeInitMethods(Object bean, String beanName) { for (Method method : bean.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(MiniPostConstruct.class)) { method.invoke(bean); } } } }这段代码虽然简单,但套上了BeanPostProcessor之后,就能完整模拟Spring最核心的两个能力:AOP代理介入时机、循环依赖解决。
4.3 用迷你Spring验证循环依赖
定义两个互相依赖的类:
@MiniComponent public class ServiceA { @MiniAutowired private ServiceB serviceB; @MiniPostConstruct public void init() { System.out.println("ServiceA init..."); } } @MiniComponent public class ServiceB { @MiniAutowired private ServiceA serviceA; @MiniPostConstruct public void init() { System.out.println("ServiceB init..."); } }用上面的容器跑一下,你会发现打印顺序符合三级缓存的演进:A实例化、A放入三级缓存、B实例化、B注入A(拿到早期引用)、B初始化、B入一级缓存、A继续初始化、A入一级缓存。
手写过一次,你会彻底明白:Bean生命周期不是一个线性“背下来”的东西,而是一套为了支撑依赖注入、AOP、循环依赖等能力设计的有序流程。面试时被问到“Bean生命周期和三级缓存的关系”,你就能从源码设计意图来回答,而不是背书。
5. 高频坑点与排查实录:实战中的生命周期陷阱
5.1 @PostConstruct不生效的几种场景
常见问题一:方法不是public。Spring的CommonAnnotationBeanPostProcessor会解析标注了@PostConstruct的方法,但要求方法不能是static,且不能带参数。如果你的方法写成private void init(),有些版本不会执行或者反射调用时异常。
常见问题二:Bean不是由Spring管理的。@PostConstruct只会对容器管理的Bean生效,如果自己new了一个对象,或者通过@Bean方法手动返回new出来的对象(没有交给Spring注册),注解自然不生效。
常见问题三:类被CGLIB代理了。Spring的@Configuration类默认用CGLIB增强,内部方法调用如果绕过代理,初始化逻辑可能执行两次或者根本不按预期执行。排查手段很简单:在方法里打日志,看执行了几次。
排查这类问题,我有个习惯:先确认Bean的创建路径,再确认BeanPostProcessor是否注册。启动参数加上-Ddebug=true,Bean的注册和增强过程会输出到控制台,比自己猜快得多。
5.2 循环依赖下AOP失效的真相
循环依赖场景里,A被B提前引用时,如果A需要AOP增强,那么代理对象在三级缓存阶段就会生成。这个代理对象是依据当时已经匹配的Advisor生成的。一般情况下没问题,但有一种极端情况:某个Advisor依赖BBean的属性(Advisor本身也参与循环依赖),此时Advisor可能还没完全初始化,导致A没有被代理或者代理不完整。
我在实际项目里遇到过一次:一个事务切面依赖了一个配置Bean,配置Bean和业务Bean形成循环依赖,最终结果是一条事务注解始终不生效。定位用了整整半天,后来用@Lazy注解打破循环才恢复正常。这也是为什么Spring官方文档建议:如果可以用@Lazy避免循环依赖,就别依赖三级缓存机制。
5.3 @PreDestroy不执行:优雅停机问题
Spring Boot应用在kill -9下不会执行销毁钩子,这一点必须要清楚。正常停机(kill默认信号,或调用ApplicationContext.close())才能触发@PreDestroy。
如果你用System.exit(0)退出Spring Boot应用,默认情况下Bean销毁流程也是不走的。正确做法是调用SpringApplication.exit(applicationContext, () -> 0)。Kubernetes部署场景下,Pod被驱逐时发的是SIGTERM,Spring Boot 2.3+默认会等待优雅停机(需要配置spring.lifecycle.timeout-per-shutdown-phase),这时候Bean的@PreDestroy才会真正执行。
排查走查清单我一般看三处:
- 进程收到的信号是SIGKILL还是SIGTERM
- 是否配置了
spring.main.register-shutdown-hook=true(默认true,但确认一下没坏处) - 销毁方法内部是否抛异常打断了后续销毁流程
5.4 面试应答的思路:从背诵到理解
以我面过上百人的经验,Bean生命周期这道题的优秀回答不是把八个阶段背全,而是能解释“为什么”。
我建议的应答结构:
先讲完整流程:实例化、属性填充、Aware回调、BeanPostProcessor前置处理、初始化三入口(@PostConstruct -> afterPropertiesSet -> init-method)、BeanPostProcessor后置处理、销毁三出口(@PreDestroy -> destroy -> destroy-method)。
再讲扩展点:指出BeanPostProcessor是Spring最核心的扩展口,AOP、事务、@Autowired全是在这个口子上实现的。
主动提三级缓存:说明循环依赖解决的本质是“实例化和初始化分离”,早期引用通过ObjectFactory从三级缓存暴露给依赖方,AOP代理在这个环节提前介入。
提一个自己踩过的坑:比如prototype Bean不执行销毁回调、@PostConstruct在代理场景下重复执行,这些真实经验会让面试官知道你确实在生产环境里排查过问题。
这样的回答完全基于生命周期理解,比单纯背八股有说服力得多。
最后分享一点个人体会
我见过太多人把Bean生命周期当作面试题来背,背完就忘。这很正常,因为生命周期不是孤立概念,它和依赖注入、AOP、循环依赖是一张网。真正把它融会贯通的方法就一个:动手写一个迷你容器,或者出问题时顺着源码把调用栈走一遍。我最初手写三级缓存的时候,代码又丑又乱,但写完后再看Spring源码,很多原来“记不住”的设计细节一下子就通了。
如果你打算深入这块,我建议从DefaultListableBeanFactory和AbstractAutowireCapableBeanFactory两个类入手,配合断点观察doCreateBean方法的执行过程。启动一个最简单的Spring Boot工程,在doCreateBean里打断点,逐步看Bean是怎么从原始对象变成最终成品的,整个过程的记忆会比任何八股都深刻。