Spring 循环依赖与提前引用
A 创建时需要 B,B 创建时又需要 A,这是一条创建依赖环。容器能否处理它,首先取决于 A 是否已经有实例,其次取决于 scope、循环引用开关和后处理器如何包装对象。拿到一个引用之后,还要区分目标是否完成初始化。
先区分三种依赖关系
创建依赖:A 的构造或属性需要 B
运行调用:A.method 调用 B.method
生命周期顺序:A 要在 B 之后启动或之前关闭只有第一类直接决定对象创建时是否会回到正在创建的节点。两个已经创建的对象互相调用,可能形成运行递归,但不是依赖注入循环;DependsOn 可以增加生命周期顺序边,却不会替对象注入字段。
构造器环中,A 尚未构造完成就要取得 B,B 又需要完整构造参数 A,没有可供提前暴露的实例。属性环允许先执行无参构造,再填充属性,所以在某些条件下可以提供提前引用。依赖与构造器循环
属性环里对象尚未准备好
完整 A 与 B 实验
下载循环依赖源码。环境为 Linux Docker、Maven 3.9.12 / Temurin 25,Spring 7.0.9,字节码目标 Java 17。使用能操作 Docker 的普通账号,只运行进程内对象;循环引用允许与否在代码中显式设置,不借用 Boot 默认值。
mkdir spring-cycles-lab
unzip spring-framework-cycles-extension-failures-lab.zip -d spring-cycles-lab
cd spring-cycles-lab
mkdir -p .m2
IMAGE=maven:3.9.12-eclipse-temurin-25
docker pull "$IMAGE"完整 src/main/java/example/FirstCycle.java:
package example;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class FirstCycle {
public static class A implements InitializingBean {
B b; boolean ready;
public void setB(B b) { this.b = b; }
public void afterPropertiesSet() { ready = true; }
public boolean ready() { return ready; }
}
public static class B implements InitializingBean {
A a; boolean seenReady;
public void setA(A a) { this.a = a; }
public void afterPropertiesSet() { seenReady = a.ready(); }
}
public static void register(AnnotationConfigApplicationContext context) {
var a = new RootBeanDefinition(A.class);
a.getPropertyValues().add("b", new RuntimeBeanReference("b"));
var b = new RootBeanDefinition(B.class);
b.getPropertyValues().add("a", new RuntimeBeanReference("a"));
context.registerBeanDefinition("a", a);
context.registerBeanDefinition("b", b);
}
public static void main(String[] args) {
try(var context = new AnnotationConfigApplicationContext()) {
context.getDefaultListableBeanFactory().setAllowCircularReferences(true);
register(context);
context.refresh();
A a = context.getBean(A.class); B b = context.getBean(B.class);
if(b.seenReady || !a.ready() || b.a != a) throw new AssertionError();
System.out.println("duringBInit=false finalReady=true sameReference=true");
}
}
}A 先实例化,填充 b 时触发 B;B 填充 a 时取得仍在创建中的 A,B 初始化观察到 A.ready 为 false。B 完成后 A 才继续初始化并改为 true。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>cycles-lab</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>7.0.9</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><parameters>true</parameters></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version></plugin>
</plugins></build>
</project>docker run --rm --user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
sh -ec 'mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache \
clean package dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
java -cp "target/classes:$(cat target/classpath.txt)" example.FirstCycle'duringBInit=false finalReady=true sameReference=trueB 持有的 A 和最终容器中的 A 是同一引用,但 B 初始化期间看到的是尚未准备好的状态。若这次调用使用的是尚未初始化的连接或配置,可能出现业务异常;将构造器改成 setter 只是改变创建方式,没有自动使初始化相互调用安全。
构建采用宿主 UID/GID,.m2 与 target 必须可写;镜像及依赖下载属于联网准备,后续实验可断网运行。企业环境通过受信 Maven settings 指定仓库和代理,不把凭据打入公开源码。依赖未完整时离线构建会失败,应先补齐缓存,不需要打开循环引用开关。Maven settings
提前引用的三个存储位置
DefaultSingletonBeanRegistry 协调单例引用:
| 结构 | 保存什么 |
|---|---|
| singletonObjects | 已完成并发布的单例引用 |
| singletonFactories | 可以按需提供提前引用的 ObjectFactory |
| earlySingletonObjects | 已经实际获取过的提前引用 |
| singletonsCurrentlyInCreation | 当前创建中的名称,用于识别重入和冲突 |
结构名称中的“三”不是三份独立业务对象。工厂延迟决定提前返回 raw 对象还是代理;只有发生重入查询,才通常需要把工厂结果放进 early 缓存。7.0.9 单例注册表
A 实例化
→ 满足 singleton + allowCircularReferences + currentlyInCreation
→ 放入 singletonFactories[A]
→ 填充 A.b,创建 B
→ B 请求 A,调用 early factory
→ earlySingletonObjects[A] 保存实际提前引用
→ B 完成,A 继续初始化
→ 确认最终引用,发布 singletonObjects[A]
→ 清理 A 的早期条目查找首先检查已完成缓存;只有目标正在创建且允许提前获取时,才尝试早期结构。单例创建还涉及锁和等待,不能把这些 Map 当作任意线程都可安全读取“半成品”的公开协议。prototype 没有相同的可复用单例早期缓存,循环通常会被拒绝。
失败分支怎样稳定出现
构造器循环与禁止属性循环
CyclesLab 的 Left 构造器需要 Right,Right 构造器需要 Left;disabled 模式则保留 FirstCycle 的属性依赖,但显式禁止循环引用。
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'CP="target/classes:$(cat target/classpath.txt)"
java -cp "$CP" example.CyclesLab constructor
java -cp "$CP" example.CyclesLab disabled'两个模式都会先记录刷新失败,再输出 rejected=BeanCurrentlyInCreationException。程序检查异常 cause 链,而非假定最外层一定是该异常;属性填充失败常被 BeanCreationException 包装,构造参数失败常被 UnsatisfiedDependencyException 包装。
拆环应先找到错误的依赖方向:
修改前:A → B → A
可选重组:
Coordinator → A
→ B
或:
A → SharedPolicy
B → SharedPolicy上层编排同时调用 A/B,或抽出两者真正共同使用的规则。确实需要延迟的依赖可以通过 ObjectProvider 或 Lazy 代理获取,但第一次调用仍会解析目标;如果把获取放回构造器,环又回到原处。是否延迟应由使用时机决定。
raw 与最终代理不一致
假设 B 已得到 raw A,某个只实现 afterInitialization 的处理器后来把 A 包装成 P。此时 B→raw A 绕开增强,其他调用者→P→A 得到增强,两条调用路径行为不同。
普通后置包装器:
B.a ─────────→ raw A
Context.a ───→ Proxy P → raw AdoCreateBean 检查已消费的提前引用、最终包装对象和实际依赖者;禁止 raw injection despite wrapping 时,这种组合会抛 BeanCurrentlyInCreationException。7.0.9 创建核对
负例把两个定义设为懒加载,在 Context 刷新后显式获取 a,以固定观察入口:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.CyclesLab late-wrap'输出 rawInjectionRejectedOnLookup=true,异常文本包含 raw version。Spring 7.0.9 的非懒单例预实例化层另有对 BeanCurrentlyInCreationException 的捕获和跳过分支,因此不能把这里的显式查询结论扩大成“所有入口都一定使 refresh 抛错”。升级或调整创建方式后,应实际获取关键 Bean 并检查依赖引用,不能只看刷新调用返回。7.0.9 预实例化
正常框架应用不应为避开检查打开 allowRawInjectionDespiteWrapping。应让包装器参与正确提前引用协议,或重组依赖消除提前包装需求。
自动代理如何复用提前引用
自动代理创建器的 getEarlyBeanReference 可以生成 P 并记住已提前处理的目标。普通 afterInitialization 阶段识别该状态后不再重复包装;doCreateBean 看到最终处理结果仍是原始目标,再采用此前的 early reference P 作为最终暴露引用。自动代理创建器
getEarlyBeanReference(A) → P → 注入 B.a
afterInitialization(A) → 不重复包装,返回 A
doCreateBean → 用早期 P 替换暴露结果
Context.a 与 B.a → 同一 P这不是 afterInitialization 再返回一个新代理的过程。执行:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.CyclesLab early-proxy'输出 earlyProxySame=true duringBInit=false finalReady=true。实验使用 DefaultAdvisorAutoProxyCreator 增强 A.ready,并同时检查代理身份和目标初始化状态。引用一致只修正了增强入口,B 初始化时 A 仍未准备好。
扩展点抢跑与故障恢复
定义阶段的 getBean 会跳过后续处理
BeanFactoryPostProcessor 运行时,普通 BeanPostProcessor 链尚未完整登记。此时为读取配置调用 getBean,会提前创建业务 Bean;后来注册的 BPP 不会自动回头重新加工已经创建好的对象。扩展点时机
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.CyclesLab premature'输出 lateProcessorHits=0。程序先由 BFPP 获取 marker,再登记晚到的实例处理器,真实地观察处理次数为零。修复应读取 Environment 或定义参数,不取得业务对象;如果处理器 Bean 的构造参数依赖业务对象,也要缩小其依赖图。
返回 BFPP/BDRPP 的 Bean 工厂常使用 static,避免先创建完整配置类。BPP 回调内部递归查整个容器同样可能触发创建环,应只处理当前目标的必要元数据。
创建失败与运行递归分别定位
创建失败记录 Bean 名、scope、构造参数或属性、定义来源和最深 cause。先画失败 A/B/C 的最小子图,再决定删哪条依赖边。换一次注解后报错名称变化,可能只是创建顺序变化,不能据此判断根因已经修好。
| 线索 | 对应关系 | 修复动作 |
|---|---|---|
| 构造参数循环 | 实例尚不存在 | 拆出上层编排或公共依赖 |
| 属性提前调用读到默认值 | 引用已存在、初始化未完成 | 初始化阶段不调用反向业务,重组依赖 |
| raw version 注入 | 提前引用与后置包装不一致 | 兼容 early 协议或移除创建环 |
| BPP 未命中 | 对象先于完整链创建 | 删除 BFPP/BPP 对业务对象的过早获取 |
| 事件不断发布同类事件 | 运行调用递归 | 修事件类型/条件和因果关系 |
| 切面反复进入自身 | 运行拦截递归 | 缩小切点,排除基础设施实现 |
| 关闭时取新 Bean 失败 | 销毁过程中重新创建 | 关闭只释放已有资源,不触发新建 |
FactoryBean 还要分别考虑工厂和产品依赖;DependsOn 可以单独构成初始化顺序环;这些条件不能只看字段箭头。原始 cause 应保留,避免记录下一轮缓存错误而丢掉首次失败。
创建失败后,Spring 会清理相关单例登记和依赖信息;自定义处理器此前取得的文件、线程或外部资源仍要按生命周期失败路径局部释放。修复配置后重新创建 Context,并执行相同模式、关键业务获取以及关闭操作。循环源码无常驻线程,进程退出即结束实验。
权威资料与规范地址
可按接口、注解和实现类查阅详细契约。
