1. 循环依赖的本质与Spring Boot中的表现形式
循环依赖在Spring Boot应用中是一个经典的设计问题,它发生在两个或多个Bean相互持有对方的引用时。想象一下两个同事A和B,A需要B完成工作才能继续自己的任务,而B同样需要A先完成工作才能开始——这就是典型的循环依赖场景。
在Spring IoC容器中,循环依赖通常表现为三种形式:
- 构造器循环依赖:两个Bean通过构造函数相互注入
- 属性循环依赖:通过@Autowired等注解相互注入属性
- 方法循环依赖:通过@Bean方法相互调用
Spring Boot默认只能解决单例作用域下的属性循环依赖(setter注入),对于构造器循环依赖会直接抛出BeanCurrentlyInCreationException。这是因为Spring的依赖注入过程分为几个阶段:
java复制// 典型循环依赖示例
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
Spring通过三级缓存机制解决属性循环依赖:
- 一级缓存:存放完整的Bean(经过实例化、属性填充、初始化)
- 二级缓存:存放早期暴露的Bean(仅实例化,未填充属性)
- 三级缓存:存放Bean工厂(用于生成早期引用)
重要提示:虽然Spring能处理属性循环依赖,但这只是技术上的补救方案。从设计角度,循环依赖通常意味着代码需要重构。
2. Spring Boot默认的循环依赖处理机制
Spring Boot继承自Spring Framework的依赖解决策略,但增加了一些自动配置的特性。当应用启动时,DefaultListableBeanFactory会处理以下流程:
2.1 属性注入的解决过程
- 创建ServiceA实例(仅调用构造函数)
- 将ServiceA的早期引用放入三级缓存
- 开始填充ServiceA的属性,发现需要ServiceB
- 创建ServiceB实例
- 将ServiceB的早期引用放入三级缓存
- 填充ServiceB属性时发现需要ServiceA
- 从三级缓存获取ServiceA的早期引用
- 完成ServiceB的属性填充和初始化
- 将ServiceB实例返回给ServiceA完成注入
- 最终完成ServiceA的初始化
java复制// Spring源码中的关键处理逻辑
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
2.2 构造器注入的限制
构造器循环依赖无法解决的根本原因在于:
- Bean的实例化需要先完成构造器调用
- 如果A需要B作为构造参数,而B又需要A作为构造参数
- 这种互相等待会导致死锁状态
java复制// 无法解决的构造器循环依赖示例
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
启动时会收到明确的错误信息:
code复制Requested bean is currently in creation: Is there an unresolvable circular reference?
3. 解决循环依赖的工程实践方案
3.1 设计层面的重构方案
- 提取公共逻辑到第三个类:
java复制@Service
public class CommonService {
// 包含A和B都需要使用的逻辑
}
@Service
public class ServiceA {
@Autowired
private CommonService commonService;
}
@Service
public class ServiceB {
@Autowired
private CommonService commonService;
}
- 使用接口分离依赖:
java复制public interface IService {
void commonMethod();
}
@Service
public class ServiceA implements IService {
@Autowired
private IService serviceB;
@Override
public void commonMethod() {
// 实现逻辑
}
}
@Service
public class ServiceB implements IService {
@Autowired
private IService serviceA;
@Override
public void commonMethod() {
// 实现逻辑
}
}
- 应用事件驱动架构:
java复制@Service
public class ServiceA {
@Autowired
private ApplicationEventPublisher publisher;
public void methodA() {
// 处理逻辑
publisher.publishEvent(new EventB(data));
}
}
@Service
public class ServiceB {
@EventListener
public void handleEventB(EventB event) {
// 处理事件
}
}
3.2 技术层面的解决方案
- 使用@Lazy延迟加载:
java复制@Service
public class ServiceA {
@Autowired
@Lazy
private ServiceB serviceB;
}
- Setter注入替代构造器注入:
java复制@Service
public class ServiceA {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
- 使用ApplicationContextAware:
java复制@Service
public class ServiceA implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
}
public void someMethod() {
ServiceB serviceB = context.getBean(ServiceB.class);
// 使用serviceB
}
}
- 调整Bean加载顺序:
java复制@DependsOn("serviceB")
@Service
public class ServiceA {
// ...
}
4. 高级场景与疑难问题处理
4.1 代理对象导致的循环依赖
当使用AOP(如@Transactional)时,Spring会创建代理对象,这可能引发特殊的循环依赖问题。解决方案:
- 使用接口代理模式(默认JDK动态代理):
java复制public interface IServiceA {
void method();
}
@Service
public class ServiceA implements IServiceA {
@Autowired
private ServiceB serviceB;
@Override
@Transactional
public void method() {
// ...
}
}
- 配置暴露AOP代理:
properties复制spring.aop.auto=true
spring.aop.proxy-target-class=false
4.2 多线程环境下的循环依赖
当循环依赖的Bean涉及异步调用时,可能出现初始化顺序问题。解决方案:
- 使用@Async时要特别注意:
java复制@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
@Async
public void asyncMethod() {
// 可能因为代理导致问题
}
}
- 确保异步方法不参与初始化:
java复制@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize();
return executor;
}
}
4.3 Spring Boot测试中的循环依赖
测试环境下循环依赖表现可能不同:
- 使用@MockBean时的特殊处理:
java复制@SpringBootTest
public class ServiceATest {
@MockBean
private ServiceB serviceB;
@Autowired
private ServiceA serviceA;
@Test
public void testMethod() {
// 测试逻辑
}
}
- 测试配置隔离:
java复制@TestConfiguration
public class TestConfig {
@Bean
@Primary
public ServiceB mockServiceB() {
return Mockito.mock(ServiceB.class);
}
}
5. 性能考量与最佳实践
5.1 循环依赖的性能影响
- 启动时间:循环依赖会增加应用启动时的Bean解析时间
- 内存占用:三级缓存机制会增加内存使用
- 运行时性能:一般不影响,但复杂的依赖关系可能导致上下文切换开销
5.2 监控与诊断
- 使用Spring Boot Actuator检查Bean依赖:
properties复制management.endpoints.web.exposure.include=beans
- 分析Bean创建顺序:
java复制@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApp.class);
app.setBannerMode(Banner.Mode.OFF);
app.addListeners(new ApplicationListener<ApplicationReadyEvent>() {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ConfigurableListableBeanFactory factory = event.getApplicationContext()
.getBeanFactory();
// 输出Bean依赖信息
}
});
app.run(args);
}
}
5.3 架构层面的预防措施
-
模块化设计:
- 遵循单一职责原则
- 使用清晰的包结构划分
- 定义明确的模块接口
-
依赖规则检查:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>com.example:module-a</exclude>
</excludes>
<includes>
<include>com.example:module-b</include>
</includes>
</bannedDependencies>
</rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>
- 代码静态分析:
- 使用SonarQube配置循环依赖检测规则
- 在CI流程中加入架构测试
- 使用ArchUnit进行架构约束测试
java复制@AnalyzeClasses(packages = "com.example")
public class ArchitectureTest {
@ArchTest
static final ArchRule no_cycles =
slices().matching("com.example.(*)..")
.should().beFreeOfCycles();
}
在实际项目中,我遇到过一个典型的循环依赖场景:订单服务需要调用支付服务,而支付服务又需要调用订单服务来更新状态。最终我们通过引入事件机制解决了这个问题——支付完成后发布事件,订单服务监听事件进行处理。这种解耦方式不仅解决了循环依赖,还使系统更容易扩展。
