各位代码战士,在 Java 开发战场中,Spring 如同「法术增幅器」—— 只需简单配置,就能快速召唤出业务所需的 Bean 对象,无需手动 new 实例、管理依赖。但你是否好奇,这背后的「Bean 召唤术」究竟遵循怎样的咒文逻辑?遇到「循环依赖诅咒」(A 依赖 B,B 依赖 A)时,又该如何破解?

今天,吾将带各位追溯 Spring 咒文的源头,拆解 Bean 召唤术的底层机制,揭秘循环依赖的破解秘辛,助你成为能驾驭 Spring 的高阶法术师!

一、Bean 召唤术的核心:Spring 容器的「咒文解析流程」

Spring 之所以能自动召唤 Bean,核心依赖「IoC 容器」(Inverse of Control,控制反转)—— 它如同 Bean 的「召唤祭坛」,负责解析配置咒文(XML / 注解)、初始化 Bean、注入依赖,让开发者无需关注对象创建细节,专注业务逻辑。

完整的 Bean 召唤流程,分为 4 大核心步骤,如同法术施展的四阶咒文:

1. 第一阶:咒文解析(BeanDefinition 注册)

  • 核心动作:Spring 容器启动时,先扫描指定路径(如 @ComponentScan 标注的包),解析类上的注解(@Component/@Service/@Controller)或 XML 配置,将每个 Bean 的「元信息」(类名、属性、依赖关系、初始化方法等)封装为「BeanDefinition 对象」,注册到「BeanDefinitionRegistry 注册表」中。

  • 形象类比:如同法术师在祭坛上记录每个召唤目标的「身份信息」,确保后续召唤时不混淆。

  • 实战案例:当扫描到 @Service("userService") 标注的类时,会生成一个 BeanDefinition,记录 Bean 名称为 userService,类型为 UserService.class,依赖的 userDao 属性等信息。

2. 第二阶:依赖注入(BeanFactoryPostProcessor 增强)

  • 核心动作:容器会调用「BeanFactoryPostProcessor」接口的实现类(如 PropertySourcesPlaceholderConfigurer),对 BeanDefinition 进行增强 —— 比如替换配置文件中的占位符(${jdbc.url})、动态修改 Bean 属性,确保 Bean 召唤前的咒文信息准确无误。

  • 形象类比:如同法术师在召唤前检查咒文,补充缺失的参数(如数据库地址、端口),避免法术失效。

3. 第三阶:Bean 实例化(doCreateBean 召唤核心)

  • 核心动作:当调用 context.getBean("userService") 或容器自动触发时,Spring 会从注册表中取出对应的 BeanDefinition,通过「反射」创建 Bean 实例(默认调用无参构造器),此步骤为「Bean 肉身的诞生」。

  • 关键注意:此时的 Bean 仅是「半成品」—— 属性未赋值,依赖未注入,如同刚召唤出的战士,还未穿戴装备、配备武器。

  • 形象类比:如同法术师念出召唤咒文,将 Bean 的「灵魂」(类信息)附着到「肉身」(实例对象)上,但尚未完成装备。

4. 第四阶:依赖注入与初始化(populateBean + initializeBean)

  • 核心动作

  1. 属性注入(populateBean):根据 BeanDefinition 中的依赖信息,将所需的其他 Bean(如 userService 依赖的 userDao)注入到当前 Bean 的属性中,完成「装备配备」;

  1. 初始化(initializeBean):调用 Bean 的初始化方法(如 @PostConstruct 标注的方法、实现 InitializingBean 接口的 afterPropertiesSet 方法),对 Bean 进行最终强化(如初始化缓存、加载配置);

  1. AOP 代理:若 Bean 被 @Transactional/@Aspect 等注解标注,Spring 会通过「动态代理」(JDK 动态代理 / CGLib)为 Bean 生成代理对象,增强其功能(如事务控制、日志记录)。

  • 形象类比:如同为召唤出的战士穿戴铠甲、配备武器,再施加「强化法术」(AOP),使其具备特殊能力(事务回滚、异常拦截)。

二、循环依赖诅咒:Bean 召唤术中的「死锁陷阱」

在 Bean 召唤过程中,最棘手的问题莫过于「循环依赖」—— 当 A 依赖 B,B 又依赖 A 时,若按常规流程召唤,会陷入「A 等 B 诞生,B 等 A 诞生」的死锁,如同两个法术师互相等待对方的咒文,导致召唤失败。

1. 循环依赖的 3 种常见场景

  • 构造器循环依赖:A 的构造器参数包含 B,B 的构造器参数包含 A(如 public A(B b) {} + public B(A a) {});

  • 字段循环依赖:A 的字段注入 B,B 的字段注入 A(如 @Autowired private B b; + @Autowired private A a;);

  • Setter 循环依赖:A 通过 Setter 方法注入 B,B 通过 Setter 方法注入 A(如 @Autowired public void setB(B b) {} + @Autowired public void setA(A a) {})。

其中,构造器循环依赖是 Spring 无法破解的「死咒」,而字段与 Setter 循环依赖,Spring 能通过「三级缓存」机制破解。

三、循环依赖破解秘辛:Spring 的「三级缓存」结界

Spring 之所以能破解字段 / Setter 循环依赖,核心是通过「三级缓存」(三个 Map 容器)构建了「临时存储 + 提前暴露」的结界,让未完成初始化的 Bean 先「临时出镜」,避免死锁。

1. 三级缓存的核心构成

缓存级别

容器名称(源码中)

存储内容

核心作用

第一级

singletonObjects

完全初始化完成的单例 Bean

供外部直接获取,如同「成品战士仓库」

第二级

earlySingletonObjects

提前暴露的「半成品 Bean」(已实例化,未注入依赖 / 初始化)

避免重复创建代理对象,如同「临时装备区」

第三级

singletonFactories

Bean 的「工厂对象」(实现 ObjectFactory 接口,getObject () 方法返回 Bean 实例)

延迟创建 Bean 的代理对象,应对 AOP 场景,如同「代理生成器」

2. 破解字段循环依赖的完整流程(以 A 依赖 B,B 依赖 A 为例)

步骤 1:召唤 A,触发三级缓存存储

  • 容器调用 getBean("a"),先查第一级缓存 singletonObjects,无 A;查第二级 earlySingletonObjects,无 A;

  • 进入 doCreateBean 流程,实例化 A(调用无参构造器),生成「半成品 A」;

  • 创建 A 的工厂对象(ObjectFactory),存入第三级缓存 singletonFactories,此时工厂的 getObject() 方法可返回 A 的实例(若有 AOP,会生成代理对象);

  • 开始为 A 注入依赖 B,调用 getBean("b"),A 的召唤暂时暂停,转向召唤 B。

步骤 2:召唤 B,触发依赖注入与缓存升级

  • 容器调用 getBean("b"),同理,实例化 B 后,将 B 的工厂对象存入第三级缓存;

  • 开始为 B 注入依赖 A,调用 getBean("a");

  • 查第一级缓存无 A,查第二级无 A,查第三级缓存有 A 的工厂对象;

  • 调用 A 工厂的 getObject() 方法,获取 A 的实例(若有 AOP,生成 A 的代理对象),将 A 从第三级缓存移到第二级缓存 earlySingletonObjects;

  • 将 A 注入到 B 中,完成 B 的属性注入与初始化,B 成为「成品 Bean」,存入第一级缓存 singletonObjects,删除 B 在第二、三级缓存的记录;

  • B 召唤完成,回到 A 的注入流程,将 B 注入到 A 中。

步骤 3:完成 A 的初始化,缓存最终存储

  • A 注入 B 后,完成自身初始化(调用 @PostConstruct 等方法),若有 AOP,生成 A 的代理对象(若第二级缓存已有 A 的代理对象,直接使用);

  • A 成为「成品 Bean」,存入第一级缓存 singletonObjects,删除 A 在第二、三级缓存的记录;

  • 至此,A 与 B 的循环依赖破解,两者均成功召唤。

3. 为什么构造器循环依赖无法破解?

  • 核心原因:构造器依赖时,Bean 的实例化(调用构造器)需要依赖对象先存在,而此时依赖对象尚未实例化 —— 比如 A 的构造器需要 B,B 的构造器需要 A,两者都卡在「实例化第一步」,无法进入三级缓存的存储流程,如同两个战士都需要对方先递武器,却都无法动弹,最终召唤失败。

  • 解决方案:避免构造器注入循环依赖,改用字段注入或 Setter 注入;若必须用构造器,可通过 @Lazy 注解延迟依赖对象的初始化(本质是生成代理对象,暂时替代真实对象)。

四、实战避坑:驾驭 Bean 召唤术的 3 个核心技巧

1. 合理选择注入方式,避免循环依赖

  • 优先用「字段注入」或「Setter 注入」处理非必须依赖,用「构造器注入」处理必须依赖(确保 Bean 初始化时依赖已存在);

  • 若出现构造器循环依赖,添加 @Lazy 注解延迟注入,示例:

@Service

public class A {

private B b;

// 延迟注入 B,初始化时生成 B 的代理对象,避免循环依赖

@Autowired

public A(@Lazy B b) {

this.b = b;

}

}

2. 巧用 @Scope 注解,避免单例缓存冲突

  • 单例 Bean(默认 @Scope("singleton"))依赖多例 Bean(@Scope("prototype"))时,多例 Bean 会被缓存为单例(导致每次获取的多例 Bean 是同一个),需用 @Lookup 注解动态获取多例 Bean:

@Service

public class SingletonService {

// 动态获取多例 Bean,每次调用都会创建新实例

@Lookup

public PrototypeService getPrototypeService() {

return null; // Spring 会自动重写此方法

}

}

3. 监控 Bean 生命周期,排查初始化异常

  • 若 Bean 召唤失败(如依赖注入为 null、初始化方法报错),可实现 BeanPostProcessor 接口,在 Bean 初始化前后打印日志,定位问题:

@Component

public class MyBeanPostProcessor implements BeanPostProcessor {

@Override

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

System.out.println("Bean 初始化前:" + beanName);

return bean;

}

@Override

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

System.out.println("Bean 初始化后:" + beanName);

return bean;

}

}