拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring IoC/DI深度解析:三级缓存与Bean生命周期

Spring IoC/DI深度解析:三级缓存与Bean生命周期

说实话,我刚用 Spring 那两年,对 IoC 的理解就停留在“不用自己 new 对象”这个层面。面试官问 Spring 怎么解决循环依赖,我还能背出“三级缓存”四个字,可再追问一句“为什么是三级,不是两级”,整个人就卡住了。后来我专门花了两周,把 Spring 容器相关源码从头读到尾,又动手写了一个极简 IoC 容器,才敢说自己真正理清了 Spring IoC&DI 这条链路。

这篇文章就是把我摸过的这些底拿出来分享。我会从 IoC 和 DI 这两个概念的本源讲起,一路拆到 Bean 生命周期、三级缓存、配置演进、手写容器的简化实现,以及日常开发里最常见的注入问题。内容不算短,但每段都有实际场景和代码支撑,适合刚学 Spring 想建立完整认知的读者,也适合准备面试前做最后梳理的人。

1. 先想明白:IoC 解决的是“谁来创建对象”

很多人第一次接触 Spring IoC,听到的就是“控制反转,把对象的创建交给容器”。字面上能听懂,但代码里仍然只是惯性写@Autowired。我建议先把“传统 new 的做法有什么问题”想清楚,IoC 和 DI 才不再是抽象概念。

1.1 从 new 到容器:一段代码的演进过程

假设有一个下单功能需要调用用户服务:

public class OrderService { private UserService userService = new UserService(); }

这段代码看起来人畜无害,但问题藏在细节里:当UserService的构造方法需要传入数据库连接池、消息队列客户端等参数时,OrderService就必须知道完整构建过程。更麻烦的是,如果UserService换成接口的另一个实现,所有写了new UserService()的地方都要改一遍。单元测试就更难受了,想 mock 一个假用户服务,得想办法把 new 出来的对象替换掉,代码会变得非常别扭。

IoC 的思路是把“创建依赖”这件事从调用方手里拿走。所有的对象统一交给一个容器管理,调用方只需要声明“我需要一个 UserService”,由容器负责创建、组装、赋值。依赖关系的所有权从调用方转移到容器,这就是“控制反转”最直接的体现。

IoC 是一个很宽泛的设计原则,而 DI(Dependency Injection,依赖注入)是 Spring 落地这个原则的具体手段。所以准确说:IoC 是思想,DI 是做法,两者不是同一个层面的东西。这也是面试里常被问到的第一道辨析题。

1.2 依赖注入的三种方式怎么选

Spring 支持三种注入方式:构造器注入、setter 注入、字段注入。我实际写代码时强烈推荐构造器注入,但不代表其他方式没有存在价值。用一个表格对比会更直观:

注入方式代码写法优点缺点推荐度
构造器注入构造方法参数依赖不可变,对象创建即完整;测试友好;不容易产生循环依赖构造函数参数多时略显臃肿最推荐
setter 注入属性 setter 方法可选依赖可以后续按需设置;便于配置框架使用对象创建后仍可能处于不完整状态按需使用
字段注入@Autowired直接标字段写起来最简洁隐蔽依赖,外部不可见;测试 mock 困难;容易掩盖循环依赖不推荐新代码使用

以一个用户注册服务为例,构造器注入写出来是这样的:

@Service public class UserRegisterService { private final UserMapper userMapper; private final MailService mailService; public UserRegisterService(UserMapper userMapper, MailService mailService) { this.userMapper = userMapper; this.mailService = mailService; } }

配合 Lombok 的@RequiredArgsConstructor还可以更简练。字段注入看起来省事,但当你把对象 new 出来写单元测试时,会发现依赖完全没法替换,只能靠 Spring 的ReflectionTestUtils硬塞,测试体验非常差。

1.3 看看依赖倒置原则

DI 能生效,底层离不开面向接口编程。OrderService不依赖UserServiceImpl这个具体类,而依赖UserService接口,容器才好把实现类注入进去。这也是 SOLID 原则里的 D——依赖倒置原则。日常开发中如果接口只有一个实现,很多人嫌麻烦直接注入具体类,短期不致命,但一旦出现第二个实现,改动成本就全回来了。

2. 一个 Bean 是怎么在容器里诞生的

理解 IoC,只停在“容器帮我 new 对象”是不够的。Spring 容器里每一个 Bean 的诞生,都要经历一条相当完整的流水线。我把这条流水线拆开,你会发现很多之前觉得“玄学”的问题,其实都可以用流程来解释。

2.1 BeanDefinition:容器的“配方”而不是成品

容器不是只拿 Class 反射创建对象,它还要知道这个 Bean 是单例还是原型,是否懒加载,构造器参数是什么,依赖哪些其他 Bean,初始化方法和销毁方法叫什么。这一堆元信息被封装成BeanDefinition——注意,BeanDefinition 是“配方”,不是“菜”。容器启动时会把配置里的每个<bean>、@Bean、@Component解析成一份份 BeanDefinition,注册到BeanDefinitionRegistry。

这也是很多人在第一个 Spring Boot 程序里感到神奇的地方:我只写了一个@SpringBootApplication,什么都没配置,为什么DataSource、RedisTemplate都能注入?因为 Spring Boot 的自动配置在启动阶段往容器里注册了大量 BeanDefinition,当你需要时才真正实例化。

2.2 ApplicationContext 到底比 BeanFactory 强在哪

很多人分不清BeanFactory和ApplicationContext的区别。简单说,BeanFactory是 Spring 容器的底层最小接口,提供最基础的 getBean、getType、containsBean 能力,长按需实例化的原则,直到调用 getBean 才创建对象。ApplicationContext是它的增强版,加了事件发布、国际化、资源加载、环境配置管理,还会在启动时就预先实例化所有非懒加载的单例 Bean,常用的类路径扫描和注解解析也都是 ApplicationContext 家族提供的。

实际项目里你基本只会接触ApplicationContext,但面试时被问到底层,别把两者讲混。比如AnnotationConfigApplicationContext就是常见的一个实现,它既可以单独在普通 Java 项目里用,也是 Spring Boot 内部容器的基础。

2.3 使用 Spring IoC 容器获取 Bean 信息的正确姿势

日常开发中“从容器里手动拿 Bean”的需求不算多,但写工具类、写测试、做框架扩展时经常遇到。最简单的方式是直接注入ApplicationContext,但更通用的是让工具类感知容器:

@Component public class SpringUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) { SpringUtils.applicationContext = applicationContext; } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } public static Object getBean(String beanName) { return applicationContext.getBean(beanName); } public static String[] getBeanNames() { return applicationContext.getBeanDefinitionNames(); } }

getBean有很多重载,按类型、按名称、按名称加类型都可以。若想调试容器里到底注册了哪些 Bean,直接调用getBeanDefinitionNames()打印一遍,效果立竿见影。我调试启动异常时就经常用这个方法,确认某个类是否真的被扫描到容器里。

2.4 Bean 生命周期全景图

拉长时间线看一个普通单例 Bean 从出生到销毁,核心步骤大概是:

  • 解析 BeanDefinition,注册进容器。
  • 通过构造器反射实例化对象。
  • 执行属性填充:@Autowired、@Resource、@Value在这里生效。
  • 回调 Aware 系列接口,比如BeanNameAware、BeanFactoryAware、ApplicationContextAware。
  • 执行BeanPostProcessor#postProcessBeforeInitialization。
  • 执行初始化回调:@PostConstruct、InitializingBean#afterPropertiesSet、自定义 init-method。
  • 执行BeanPostProcessor#postProcessAfterInitialization,AOP 动态代理就是在这个阶段生成的。
  • Bean 进入就绪状态,供业务代码使用。
  • 容器关闭时执行销毁逻辑:@PreDestroy、DisposableBean#destroy、自定义 destroy-method。

这个流程看起来繁琐,但每一步都留了“钩子”。比如你想在 Bean 初始化前后统一做参数校验、日志埋点或生成代理,就能通过BeanPostProcessor介入。理解生命周期之后,再看 Spring 的 AOP、事务、MyBatis Mapper 代理等机制,都会有一种“原来是挂在这个环节”的通透感。

3. 循环依赖与三级缓存:Spring 里最容易翻车也最常被问的设计

循环依赖是面试必考点,也是最容易暴露一个人是否真正理解 Spring 容器的问题。我面试别人的时候,从来不要求背三级缓存,而是先问一句:“两个 Bean 互相依赖,Spring 怎么保证不无限递归?”能答上来的人,通常是真的想过容器的工作流程。

3.1 什么样的循环依赖能解决

先定义问题。假设有两个单例 Bean:

@Service public class ServiceA { @Autowired private ServiceB serviceB; } @Service public class ServiceB { @Autowired private ServiceA serviceA; }

Spring 创建 ServiceA 时需要 ServiceB,创建 ServiceB 又需要 ServiceA,依赖形成闭环。解决的关键在于:Spring 允许在 ServiceA 的属性还没填充完,就提前暴露一个“早期引用”,让 ServiceB 先拿着这个不完整的对象用,等 ServiceA 继续完成后续步骤。

但注意,Spring 默认只能解决“单例 + setter/字段注入”的循环依赖,构造器注入造成的循环依赖无法解决。原因很直观:构造器注入在 Bean 实例化阶段就必须拿到完整依赖,这时对象都还不存在,谈不上提前暴露半成品。所以我在项目里推构造器注入时总会补一句:这不仅能保证依赖完整,还能顺带避免构造器循环依赖,让问题在启动时直接暴露。

3.2 三级缓存各层到底放了什么

三级缓存是三个 Map,各司其职:

// 一级:存放完整可用的单例 Bean Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); // 二级:存放提前暴露的早期 Bean 引用(还没完成属性填充) Map<String, Object> earlySingletonObjects = new HashMap<>(); // 三级:存放 ObjectFactory,用来生成早期引用 Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

一级缓存是最终产物,所有人都能拿到。二级缓存存的是“半成品”。三级缓存存的是一个ObjectFactory函数式对象,调用getObject()时才真正生成早期引用,这一步是三级缓存的核心设计。

我用一个普通场景走一遍流程:

  • 创建 ServiceA,实例化后把它的ObjectFactory放入三级缓存。
  • 填充 ServiceA 属性时发现需要 ServiceB,于是调用 getBean(ServiceB)。
  • 创建 ServiceB,实例化后同样把ObjectFactory放入三级缓存。
  • 填充 ServiceB 属性时发现需要 ServiceA,这时一级、二级缓存都没有 ServiceA。
  • 容器查到 ServiceA 的singletonFactory,调用getObject()拿到一个早期引用,放入二级缓存,并从三级缓存移除。
  • ServiceB 成功拿到 ServiceA 的早期引用,完成属性填充、初始化,成为完整 Bean 放入一级缓存。
  • ServiceA 继续执行,也要从容器拿 ServiceB,此时一级缓存已经有完整 ServiceB。
  • ServiceA 完成初始化,放入一级缓存。

整个过程里,ServiceB 实际拿到的确实是 ServiceA 的不完整引用,但只要最终 ServiceA 引用地址不变,后面属性补全不影响 ServiceB 使用。这正是 Spring 敢提前暴露对象的底气。

3.3 为什么是三级,不是两级

面试里最刁钻的问题在这里。有人会说,既然二级缓存已经能放早期引用,三级缓存中的 ObjectFactory 是不是多余?关键就在 ObjectFactory 的延迟能力。

如果一个 Bean 需要被 AOP 代理,Spring 最终放进一级缓存的是代理对象。如果只有二级缓存,在实例化后立即生成早期引用,那此时对象还没执行BeanPostProcessor,代理还没生成,ServiceB 拿到的很可能是一个“裸对象”。等 ServiceA 填完属性生成代理后,内存里同一个 Bean 出现了两个不同对象:ServiceB 持有裸对象,容器里却是代理对象,这就会导致 AOP 失效或类型不一致。

三级缓存的 ObjectFactory 可以在“真正需要提前引用”时调用getEarlyBeanReference,让后置处理器有机会生成代理。如果没有循环依赖,这个 ObjectFactory 不会被调用,代理就在正常生命周期最后阶段生成;如果存在循环依赖,提前暴露时会直接生成代理,确保所有引用拿到的都是代理,容器最终暴露的也是同一个代理。把选择延后到“确实需要”的时刻,本质上是一个延迟决策的设计。

protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && this.singletonsCurrentlyInCreation.contains(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }

这段是从 Spring 5 之前的源码里提炼的简化版,最新的 Spring 在加锁方式上有变化,但核心思路没有变。能看懂这段代码,基本比只背“三级缓存四个字”的候选人扎实得多。

3.4 循环依赖与 AOP 的暗坑

一旦循环依赖和 AOP 同时出现,情况会更加微妙。我记得有次排查一个问题:一个涉及事务的 Service A 与另一个普通 Service B 循环依赖,结果 A 里调自己的方法时事务失效了。排查到最后发现,B 持有的 A 是原始对象而不是 AOP 代理对象。

原因就是上面讲的:如果三级缓存的getObject()被提前触发,生成的代理确实会被各方拿到;但如果没有循环依赖,代理会在初始化之后生成,不会有问题。问题往往出在“这个 Bean 本身不需要循环依赖,但某个 BeanPostProcessor 在早期阶段触发了引用”,间接导致代理逻辑变得混乱。Spring 官方其实不太推荐循环依赖,Jakarta 规范语境下的架构设计更是建议直接打破循环。我从实际开发里得到的经验:循环依赖能不用就不用,重构手段包括把互相依赖的方法拆分到不同服务、引入事件解耦、或者用@Lazy把一边的依赖延迟加载来切断循环。

4. 从 XML 到 Java Config:DI 配置方式的演进

Spring 早期的配置方式是 XML,很多老项目现在还有<bean>标签,我对 XML 时代的印象就是“写配置写到手抽筋”。但现在看,XML 方式的演进其实很好地向我们展示了 IoC 容器解决问题的方式:把对象关系变成可配置的数据,而不是硬编码在类里。

4.1 XML 时代

一个典型的 XML 配置长这样:

<bean id="userService" class="com.example.service.UserService"> <property name="userDao" ref="userDao"/> </bean> <bean id="userDao" class="com.example.dao.UserDao"/>

property标签表示 setter 注入,constructor-arg表示构造器注入。这种方式的优点是配置与代码分离,缺点是对象一多配置体积迅速膨胀,而且没有编译期检查,属性名写错了只能等运行时报错。

4.2 注解驱动把 IoC 变成了“声明式”

后来 Spring 2.5 引入了注解,@Component、@Service、@Repository、@Controller本质上是同一个注解的不同语义变体,核心都是标记这个类要被容器管理。配合@ComponentScan扫描包路径,把@Autowired标在字段或构造器上,Spring 会在属性填充阶段自动注入依赖。

我一般会提醒新人:@Autowired默认按类型查找,如果同一类型有多个候选 Bean,就会抛NoUniqueBeanDefinitionException。解决办法有@Primary指定主候选,或者用@Qualifier("beanName")精确指定名称。@Resource是 JSR-250 标准里的注解,默认按字段名查找,再按类型查找。如果选@Resource,要注意它的语义和@Autowired不完全一样,别混用后凭感觉猜。

4.3 Java Config 与 Spring Boot 自动装配

Spring 3.0 引入了@Configuration和@Bean:

@Configuration public class AppConfig { @Bean public UserService userService(UserDao userDao) { return new UserService(userDao); } }

写法集中在 Java 代码里,有编译期类型检查,重构时也能被 IDE 正确识别。到了 Spring Boot,配置进一步自动化:@SpringBootApplication里内置了@ComponentScan和@EnableAutoConfiguration,自动配置通过@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,在类路径上发现相关依赖就把对应 Bean 注册进容器。

这也是很多人的第一个 Spring Boot 程序能直接启动的原因:你引入spring-boot-starter-web,自动配置发现类路径有Servlet相关类,于是帮你注册了DispatcherServlet和内嵌 Tomcat;你引入spring-boot-starter-data-redis,自动配置发现RedisTemplate相关类存在,就帮你配置连接工厂。本质上,这些全部都是 IoC 容器在背后批量注册 BeanDefinition 的产物。调试时可以在application.yml里加debug: true,启动日志会打印一份自动配置报告,能看到哪些自动化配置生效了、哪些被排除了,强烈建议自己试一次。

5. 手写一个简化版 IoC 容器到底有多难

我曾经把“手写 Spring”当作验证自己理解程度的手段。写完之后最大的感受是:框架里很多看似冗余的设计,真的都是必要的。这里我把一个迷你容器的实现思路拆开,你照着走一遍,对 IoC 的理解会比读十篇原理文章都牢。

5.1 定义最小功能目标

不追求完整,先做四件事:

  • 扫描指定包下所有带@Component注解的类。
  • 把它们注册成类似 BeanDefinition 的元信息。
  • 实例化对象并注入@Autowired标注的依赖。
  • 用三级缓存解决单例循环依赖。

我用一个MiniApplicationContext承载全部逻辑,内部字段包括三个缓存 Map、一个“当前正在创建”的集合、一个 BeanDefinition 的注册表:

public class MiniApplicationContext { // Bean 名称 -> BeanDefinition(简化版用 Class 代替) private final Map<String, Class<?>> beanDefinitions = new ConcurrentHashMap<>(); // 三级缓存 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 Set<String> singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(); public MiniApplicationContext(Class<?> configClass) throws Exception { // 扫描 configClass 所在的包,注册 BeanDefinition scan(configClass.getPackageName()); // 模板方法:逐个实例化非懒加载单例 Bean for (String beanName : beanDefinitions.keySet()) { getBean(beanName); } } public Object getBean(String beanName) throws Exception { // 第一级缓存命中直接返回 if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } // 第二级缓存命中说明正在创建中,实际上是早期引用 if (earlySingletonObjects.containsKey(beanName)) { return earlySingletonObjects.get(beanName); } // 第三级缓存存在,则调用 ObjectFactory 获取并升级到二级缓存 if (singletonFactories.containsKey(beanName)) { Object earlyObject = singletonFactories.get(beanName).getObject(); earlySingletonObjects.put(beanName, earlyObject); singletonFactories.remove(beanName); return earlyObject; } // 如果正在创建则说明发生了重复创建 return createBean(beanName); } }

扫描包本来可以用 Spring 现成的ClassPathScanningCandidateComponentProvider,但手写时我用最简单的方式:递归读取文件系统里 class 文件路径,再通过反射加载类,检查是否有@Component注解。这部分代码不复杂,但能让你体会到“扫描包”背后其实没有魔法。

5.2 实例化与注入的反射套路

拿到一个 BeanDefinition 后,创建对象分两步:实例化、填充属性。

private Object createBean(String beanName) throws Exception { Class<?> clazz = beanDefinitions.get(beanName); // 标记正在创建,用于循环依赖判断 singletonsCurrentlyInCreation.add(beanName); // 实例化 Object instance = clazz.getDeclaredConstructor().newInstance(); // 提前暴露 ObjectFactory 到三级缓存 singletonFactories.put(beanName, s -> instance); // 属性填充 populateBean(instance); // 完成创建,升级到一级缓存,移除中间缓存 singletonObjects.put(beanName, instance); singletonFactories.remove(beanName); earlySingletonObjects.remove(beanName); singletonsCurrentlyInCreation.remove(beanName); return instance; } private void populateBean(Object instance) throws Exception { for (Field field : instance.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); // 按字段类型找到 Bean 名称,再递归 getBean Object dependency = getBean(field.getType().getSimpleName()); field.set(instance, dependency); } } }

如果两个 Bean 互注,走到populateBean时会递归 getBean,此时三级缓存的早期引用就会兜底。我这个极简版本里 ObjectFactory 只是返回原始对象,没有做 AOP 处理;真正的 Spring 会在工厂方法里调用getEarlyBeanReference生成代理。你用这个简化模型去反推 Spring 的源码,会发现很多逻辑完全对得上。

5.3 手写之后我才明白的事

写完后我第一时间翻了一遍 Spring 的DefaultSingletonBeanRegistry,之前觉得绕的三级缓存代码一下子就能看懂了。手写项目不需要很大,核心逻辑加起来两三百行就能跑起来,但写之前你要能回答清楚几个问题:Bean 是怎么被发现的?默认实现和别名怎么处理?容器怎么判断 Bean 是否创建到一半?这些恰好是把 IoC 从“知识点”变成“能力”的关键。

很多人在学习阶段喜欢背面试题,但“手写 Spring”类的问题一旦出现,立刻露馅。我的建议是:如果时间有限,至少要动手写一个极简版本,哪怕只处理扫描、实例化、注入三步,也足够你在面试时从容地说出容器工作流程。

6. 常见问题与排查技巧实录

这部分是我在项目里踩过、也帮别人排查过的真实问题,按出现频率从高到低整理。理解了前面原理,这些错误基本都能定位到位。

6.1 注入失败类错误

这类报错通常出现在启动阶段,核心是找不到对应的 Bean 或者找到了多个 Bean。

异常信息关键片段对应问题处理思路
NoSuchBeanDefinitionException容器里根本没有这个类型的 Bean检查包扫描路径、类上有没有@Component家族注解、@Bean方法是否被@Configuration包裹
NoUniqueBeanDefinitionException同一类型找到多个 Bean在候选 Bean 上加@Primary,或注入处用@Qualifier("beanName")指定
BeanCurrentlyInCreationException构造器循环依赖把其中一个依赖改成 setter/字段注入,或加@Lazy拆环
UnsatisfiedDependencyException泛化错误,底层嵌套真实失败原因不要只看外层异常,从 Cause 往深处读才是真正问题
BeanNotOfRequiredTypeException类型不匹配,比如动态代理导致了类型不一致检查是否把代理类当成原始类使用,必要时用接口接收

读异常信息时有一个习惯很关键:Spring 的异常链往往很长,初学者看到第一行就慌了。实际排查时我会优先看 Caused by 层,那里才是第一次抛错的真正位置。

6.2 @Autowired 注入后是 null 怎么办

这是我被问过最多的问题之一,几乎每个 Spring Boot 教程下面都会有留言问“为什么我拿到的 service 是 null”。常见的几个原因:

  • 对象不是由 Spring 管理的,而是直接new出来的。new出来的对象不会经过容器,@Autowired自然无效。
  • 静态字段或静态代码块里使用注入对象。Spring 不支持直接注入静态字段,常见做法是把容器工具类注册进 Spring,再通过静态方法访问。
  • 在@PostConstruct之前的某个时机访问了注入对象,此时属性填充可能都还没完成。
  • 异步线程或定时任务里通过new创建的类访问注入对象。

最常见也最容易踩的其实是第一和第三种。解法也很直接:能用 Spring 管理就让 Spring 管,不要在普通工具类中硬编码依赖;需要静态访问时,通过实现ApplicationContextAware持有容器再获取 Bean,这才是正确姿势。

6.3 面试高频追问怎么接

“IoC 和 DI 的关系”是最基础的,能分清“IoC 是设计原则、DI 是落地手段”就够了。“BeanFactory 和 ApplicationContext 的区别”要把懒加载和增强能力讲清楚。“三级缓存原理”重点说清三级缓存各自的职责,以及为什么第三级要放 ObjectFactory。如果面试官再追问“AOP 和 IoC 的关系”,可以结合BeanPostProcessor在初始化后阶段生成代理来讲,说明 AOP 其实是嵌套在 Bean 生命周期里的插件机制。

“Spring 创建 Bean 的流程”这个问题,我会按生命周期主线回答,从BeanDefinition注册、实例化、属性填充、Aware 回调、BeanPostProcessor、初始化,一直讲到销毁。边回答边结合源码里的典型类名,比如AbstractAutowireCapableBeanFactory#doCreateBean,面试官一听就知道你是真读过源码。

最后再分享一点实际体会

写这篇文章之前,我又把自己那个迷你容器项目重新看了一遍,发现很多当时觉得“很简单”的设计,后来都在 Spring 源码里找到了对应原型。Spring IoC&DI 要说难,难在理解容器创建 Bean 时的决策顺序;要说简单,只要你动手写过一次,很多问题都会自然消解。

我给你的建议很简单:不要只刷面试题,去写一个能跑起来的手写 Spring 迷你版。遇到报错就翻源码,遇到不懂就打断点,把DefaultSingletonBeanRegistry和AbstractAutowireCapableBeanFactory这两个类的关键方法打印出来看。等你亲手处理过一个“循环依赖导致启动失败”的案例,再回头看这些原理,会有一种完全不同的感觉。

返回列表