Spring IOC:容器启动那几秒发生了什么
昨天还能启动的项目,升级 Spring Boot 之后启动就炸,报错里滚出 BeanCurrentlyInCreationException,还有一句 allow-circular-references。改动只有一个版本号。要讲清这件事,得回到容器启动的那几秒,看 Bean 是怎么被造出来、装好、放进容器的。IOC 这个词吓人,事情本身很朴素,把 new 这件事从业务代码挪进了容器。
启动流水线
容器启动干的活是一条流水线,每个 Bean 都要走一遍。
第一站,扫描。包路径下带 @Component、@Service 这些注解的类被扫出来,生成 BeanDefinition,对象的图纸,记着类名、作用域、依赖关系。
第二站,实例化。照图纸调构造方法,对象出生,此时字段还是 null。
第三站,依赖填充。看字段上的 @Autowired,去容器里找对应的 Bean 塞进来,依赖注入就发生在这一站。
第四站,初始化。@PostConstruct、InitializingBean、init-method 在这里执行,你的业务初始化逻辑跑完,Bean 才算成品。
第五站,进单例池。放进一个大 Map,之后所有地方 getBean 拿的都是这一个实例。
循环依赖怎么解
A 的字段要 B,B 的字段要 A,两边都要等对方先出生,先有鸡还是先有蛋。容器的答案是「提前曝光」,A 实例化完(字段还空着)就先把引用挂出去,让 B 能拿到 A 的半成品,B 装配完回来再把 A 的字段填上。
这套提前曝光就是三级缓存。一级放成品,二级放早期引用(半成品),三级放一个工厂,处理需要 AOP 代理的早期引用。顺序是这样的。
能提前曝光,前提是对象「生出来但字段没填」这个中间态可以被别人用。字段注入和 setter 注入满足这个条件,构造器注入不满足,构造方法参数必须是成品,A 的构造要 B、B 的构造要 A,绕不开,所以构造器注入的循环依赖无解,只能改设计。
Spring Boot 2.6 起默认禁止循环依赖(allow-circular-references 默认 false),开头那个报错就是这个改动。版本口径,Spring Framework 本身仍有开关,但新项目的态度是把循环依赖当设计问题修,别当配置问题绕。
报错都在流水线上
启动报错对照流水线,位置就清楚了。
BeanCurrentlyInCreationException,创建中又请求创建,循环依赖,回上一节。NoSuchBeanDefinitionException,图纸都没有,扫描没扫到,包路径、注解、条件装配三处查。UnsatisfiedDependencyException,图纸有但依赖填不上,@Autowired 的类型找不到或者构造参数缺失。NoUniqueBeanDefinitionException,同类型多张图纸,缺 @Qualifier 或 @Primary。记住一句话:启动报错先定位在流水线的哪一站,再看那一站的输入。
亲手跑一个迷你容器
下面用一段 Java 写一个迷你 IOC,反射注入加二级缓存,跑起来看循环依赖怎么被解开。直接运行 java MiniIoC.java(JDK 11+)。
// 直接运行:java MiniIoC.java
// 迷你 IOC:反射注入,二级缓存提前暴露解循环依赖。
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class MiniIoC {
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@interface Inject {
}
static class ServiceA {
@Inject
ServiceB b;
String name() {
return "A";
}
}
static class ServiceB {
@Inject
ServiceA a;
String name() {
return "B";
}
}
private final Map<String, Object> singletons = new HashMap<>(); // 一级:成品
private final Map<String, Object> early = new HashMap<>(); // 二级:早期引用
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) throws Exception {
String key = type.getName();
if (singletons.containsKey(key)) {
return (T) singletons.get(key);
}
if (early.containsKey(key)) {
System.out.println(" [二级缓存] " + type.getSimpleName() + " 的早期引用被取走");
return (T) early.get(key);
}
// 实例化,字段还空着
T instance = type.getDeclaredConstructor().newInstance();
System.out.println(" [实例化] " + type.getSimpleName() + ",字段还是 null");
early.put(key, instance); // 提前曝光,循环依赖的救星
// 依赖填充
for (Field f : type.getDeclaredFields()) {
if (f.isAnnotationPresent(Inject.class)) {
f.setAccessible(true);
System.out.println(" [填充] " + type.getSimpleName() + "." + f.getType().getSimpleName());
f.set(instance, getBean(f.getType()));
}
}
early.remove(key);
singletons.put(key, instance); // 成品进一级
return instance;
}
public static void main(String[] args) throws Exception {
MiniIoC container = new MiniIoC();
System.out.println("请求 ServiceA:");
ServiceA a = container.getBean(ServiceA.class);
System.out.println("装配结果:a.b == " + (a.b != null ? "B" : "null")
+ ",b.a == " + (a.b.a == a ? "A 自己(同一个实例)" : "别的"));
System.out.println("循环依赖被二级缓存解开了。");
}
}
适用边界
这一篇管启动主线和循环依赖。Bean 的作用域(prototype 不走这套)、FactoryBean、@Configuration 的 CGLIB 增强,以及 BeanPostProcessor 的完整扩展点,是另外的题。迷你容器没有三级缓存里的工厂(那层是给 AOP 代理留的,下一篇讲)。原则就一条:启动报错先对流水线的站,循环依赖先看注入方式,字段和 setter 能解,构造器只能改设计。
本文深入剖析Spring IOC容器启动时的Bean创建流水线,涵盖扫描、实例化、依赖注入、初始化及单例注册五个关键阶段;重点解析循环依赖的三级缓存机制与构造器注入的不可解性;结合Spring Boot 2.6默认禁用循环依赖的变更,说明常见启动异常(如BeanCurrentlyInCreationException)的定位方法;并通过迷你容器示例直观展示依赖解析过程。
262

被折叠的 条评论
为什么被折叠?



