Spring 源码没你想的那么难:IoC 和 AOP 的核心就这两条线
场景:一个诡异的循环依赖报错
先从一个真实案例说起。某个订单服务在启动时抛了 BeanCurrentlyInCreationException,日志指向 OrderService 和 UserService 互相引用。网上答案一搜一大把:"加 @Lazy 就好了"。但加完注解启动是过了,两个月后某个深夜,线上出现延迟初始化导致的空指针——@Lazy 把代理对象的创建推迟到了第一次调用,而那个调用恰好发生在事务拦截器还没绑定完的间隙。
问题根源不在 @Lazy,而在对 Spring IoC 容器创建 Bean 的三个阶段没吃透。
第一阶段:BeanDefinition 的收集与合并
容器启动时,ConfigurationClassPostProcessor 会扫描所有 @Component、@Bean、@Import,把每个类的元信息封装成 BeanDefinition。这一步不创建对象,只记"配方"。
关键点在 AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 的分工:前者处理显式注册的配置类,后者处理包扫描。两者最终都调用 BeanDefinitionRegistry.registerBeanDefinition() 把定义塞进 DefaultListableBeanFactory 的 beanDefinitionMap。
// ConfigurationClassPostProcessor 的核心逻辑(精简)
public void processConfigBeanDefinitions(Registry registry) {
// 1. 找出所有 @Configuration 类
// 2. 解析 @ComponentScan、@Import、@Bean 方法
// 3. 生成对应的 BeanDefinition 并注册
// 4. 如果有新的 @Configuration 类被引入,循环处理
}
这里的坑:@Configuration 类本身也是 Bean,且默认是 proxyBeanMethods = true,意味着配置类会被 CGLIB 代理,@Bean 方法之间的调用会走容器查找而不是直接 new。如果你把配置类改成 @Configuration(proxyBeanMethods = false),@Bean 方法里手动 new 的对象不会进容器,循环依赖处理方式直接变样。
第二阶段:实例化与属性填充——循环依赖的真相
AbstractAutowireCapableBeanFactory.doCreateBean() 是核心。流程分三步:
- 实例化:
createBeanInstance()通过构造器或工厂方法生成原始对象(此时属性全是 null) - 属性填充:
populateBean()处理@Autowired、@Resource、@Value - 初始化:
initializeBean()执行@PostConstruct、InitializingBean、AOP 代理生成
循环依赖能解,靠的是三级缓存:
// DefaultSingletonBeanRegistry 中的三个 Map
private final Map<String, Object> singletonObjects; // 一级:成品 Bean
private final Map<String, Object> earlySingletonObjects; // 二级:半成品(原始对象)
private final Map<String, ObjectFactory<?>> singletonFactories; // 三级:对象工厂
OrderService 先创建,实例化完成后放入三级缓存。填充属性时发现需要 UserService,容器转去创建 UserService,后者填充属性时又需要 OrderService——此时从三级缓存拿到 ObjectFactory,调用 getEarlyBeanReference() 得到 OrderService 的早期引用(可能是个代理),注入给 UserService。UserService 创建完成后,OrderService 继续填充剩余的属性。
关键点:三级缓存里的 ObjectFactory 在 getEarlyBeanReference() 时可能触发 AOP 代理创建。如果 OrderService 有 @Transactional,早期引用拿到的就是代理对象;如果没有 AOP,拿到的就是原始对象。这就是为什么循环依赖 + AOP 会偶发"类型不匹配"——你在 UserService 里注入的是代理,但容器最终放入一级缓存的是另一个代理实例。
最容易踩的坑:构造器注入无法解决循环依赖。因为构造器注入在实例化阶段就要拿到依赖,此时对象还没放入三级缓存,根本无从查起。所以遇到构造器循环依赖,唯一正解是重构代码,而不是加 @Lazy 糊弄。
第三阶段:初始化与 AOP 代理的生成时机
回到开头的 @Lazy 空指针问题。@Lazy 的本质是给依赖注入一个 TargetSource 的代理,真正解析目标 Bean 的时机推迟到第一次方法调用。但 Spring 的 AOP 代理(比如事务)是在 initializeBean() 阶段通过 AbstractAutoProxyCreator.postProcessAfterInitialization() 创建的。
// AbstractAutoProxyCreator 的关键逻辑
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean != null) {
// 检查是否有匹配的 Advisor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// 创建代理对象
return createProxy(bean.getClass(), beanName, specificInterceptors);
}
}
return bean;
}
这里有个顺序问题:@Lazy 代理在属性填充阶段注入,AOP 代理在初始化阶段创建。如果 @Lazy 的目标 Bean 本身需要事务代理,那么注入的 @Lazy 代理内部持有的 TargetSource 会在第一次调用时才去容器获取真正的代理。这个"真正的代理"此时还没创建完——因为 UserService 的 initializeBean() 还没执行到。
解决方案很简单:不要在 @Lazy 的 Bean 上同时依赖事务等 AOP 功能,或者改用 ObjectFactory / Provider 注入,它们不创建代理,只做查找,更安全。
性能调优:为什么启动慢
容器启动慢,绝大多数情况不是反射慢,而是 BeanPostProcessor 太多。每个 BeanPostProcessor 的 postProcessBeforeInitialization 和 postProcessAfterInitialization 都会在每个 Bean 上执行。项目里有 500 个 Bean、20 个 BeanPostProcessor,就是 10000 次回调。
一个真实调优案例:某微服务启动耗时 45 秒,用 -verbose:class 看类加载,发现 @ConfigurationProperties 的 ConfigurationPropertiesBindingPostProcessor 在每个 Bean 上都做了属性绑定检查。优化方式是把 @ConfigurationProperties 的类从组件扫描里排除,改为 @EnableConfigurationProperties 显式注册,启动时间降到 28 秒。
另一个常见瓶颈是 @ComponentScan 的过滤条件过宽。默认扫描 @Component 派生的所有注解,如果包路径过宽,会把不该扫的第三方 jar 里的类也扫进来。用 includeFilters / excludeFilters 精确控制,能显著减少 BeanDefinition 的收集时间。
收尾
Spring 的 IoC 和 AOP 没有魔法,就是三个 Map 加一堆回调。循环依赖靠三级缓存,AOP 代理在 postProcessAfterInitialization 里创建,启动慢大多是 BeanPostProcessor 回调太多。
下一步建议:打开 AbstractApplicationContext.refresh() 方法,从 invokeBeanFactoryPostProcessors 开始跟一遍断点,把每个 BeanPostProcessor 的注册时机和调用链画出来。不用读完所有代码,只需要弄清 ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、AbstractAutoProxyCreator 这三者的执行顺序,Spring 源码的骨架就搭起来了。
本文关键词:Spring IoC、三级缓存、AOP代理、BeanPostProcessor、循环依赖