SpringIOC-容器启动那几秒发生了什么

本文深入剖析Spring IOC容器启动时的Bean创建流水线,涵盖扫描、实例化、依赖注入、初始化及单例注册五个关键阶段;重点解析循环依赖的三级缓存机制与构造器注入的不可解性;结合Spring Boot 2.6默认禁用循环依赖的变更,说明常见启动异常(如BeanCurrentlyInCreationException)的定位方法;并通过迷你容器示例直观展示依赖解析过程。

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 拿的都是这一个实例。

扫描
生成 BeanDefinition

实例化
调构造方法

依赖填充
注入字段

初始化
@PostConstruct

进单例池
单例 Map

循环依赖怎么解

A 的字段要 B,B 的字段要 A,两边都要等对方先出生,先有鸡还是先有蛋。容器的答案是「提前曝光」,A 实例化完(字段还空着)就先把引用挂出去,让 B 能拿到 A 的半成品,B 装配完回来再把 A 的字段填上。
这套提前曝光就是三级缓存。一级放成品,二级放早期引用(半成品),三级放一个工厂,处理需要 AOP 代理的早期引用。顺序是这样的。

创建 A:实例化,字段还空

把 A 的早期引用放入二级缓存

填充 A.b,去创建 B

创建 B:填充 B.a,从二级缓存拿到 A

B 成品,回到 A 继续填字段

A 成品,双双进单例池

能提前曝光,前提是对象「生出来但字段没填」这个中间态可以被别人用。字段注入和 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 能解,构造器只能改设计。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值