SpringBoot 2.6.0 + 循环依赖破解:从原理到多场景解决方案
在 SpringBoot 2.6.0 版本的发布日志中,一个看似微小的变更引发了广泛关注:默认禁用循环依赖自动处理。这一调整导致大量升级项目突然抛出BeanCurrentlyInCreationException,迫使开发者重新审视代码中的依赖设计。循环依赖并非洪水猛兽,但其隐蔽性可能导致系统启动不稳定和事务管理异常。本文将从 Spring 容器的依赖注入原理出发,详解 2.6.0 + 版本中循环依赖的成因、风险及五种实战解决方案,帮助开发者在保持代码灵活性的同时遵循 Spring 最佳实践。
依赖困境:循环引用的成因与容器行为变化
Spring 中的循环依赖指两个或多个 Bean 相互持有对方的引用,形成闭环依赖关系。在 2.6.0 之前,Spring 容器通过三级缓存机制自动处理这种情况,但这一机制并非万能,可能掩盖代码设计缺陷。
循环依赖的典型场景通常表现为服务层之间的相互调用:
// 订单服务依赖用户服务
@Service
public class OrderService {
private final UserService userService;
// 构造函数注入
public OrderService(UserService userService) {
this.userService = userService;
}
public void createOrder(Long userId) {
// 调用用户服务验证用户
User user = userService.getUserById(userId);
// 处理订单创建逻辑
}
}
// 用户服务依赖订单服务
@Service
public class UserService {
private final OrderService orderService;
// 构造函数注入
public UserService(OrderService orderService) {
this.orderService = orderService;
}
public User getUserById(Long id) {
// 基础用户查询逻辑
User user = userRepository.findById(id);
// 调用订单服务获取用户最近订单
List<Order> recentOrders = orderService.getRecentOrders(id);
user.setRecentOrders(recentOrders);
return user;
}
}
在 SpringBoot 2.6.0 之前,这段代码能正常启动,因为容器通过以下三级缓存机制处理:
- 一级缓存(singletonObjects):存储完全初始化的 Bean
- 二级缓存(earlySingletonObjects):存储提前暴露的原始 Bean
- 三级缓存(singletonFactories):存储 Bean 工厂,用于生成代理对象
当检测到循环依赖时,容器会从三级缓存中提前暴露未完全初始化的 Bean,打破依赖闭环。但在 2.6.0 版本中,这一机制被默认关闭,上述代码会抛出:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference?
禁用自动处理的深层原因包括:
- 循环依赖可能导致事务代理失效,@Transactional 注解不生效
- 提前暴露的 Bean 可能处于不一致状态,引发初始化顺序问题
- 掩盖了代码设计中的职责边界不清问题
Spring 官方建议通过重构代码消除循环依赖,而非依赖容器的自动处理。但在复杂系统中,完全消除可能代价过高,因此需要掌握多种针对性解决方案。
解耦之道:代码重构与依赖注入调整
解决循环依赖的根本方法是通过代码重构打破依赖闭环,明确 Bean 之间的职责边界。当重构成本较高时,也可通过调整依赖注入方式临时规避。
职责分离重构是最彻底的解决方案,通过引入中间层拆分相互依赖的功能:
// 1. 提取共享数据模型
public class UserOrderDTO {
private User user;
private List<Order> recentOrders;
// getter/setter
}
// 2. 引入协调服务处理跨领域逻辑
@Service
public class UserOrderCoordinator {
private final UserService userService;
private final OrderService orderService;
public UserOrderCoordinator(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
// 整合用户与订单信息的方法
public UserOrderDTO getUserWithOrders(Long userId) {
User user = userService.getUserById(userId);
List<Order> orders = orderService.getRecentOrders(userId);
UserOrderDTO dto = new UserOrderDTO();
dto.setUser(user);
dto.setRecentOrders(orders);
return dto;
}
}
// 3. 重构用户服务,移除对订单服务的依赖
@Service
public class UserService {
// 仅保留用户自身相关逻辑,不依赖OrderService
public User getUserById(Long id) {
return userRepository.findById(id);
}
}
// 4. 重构订单服务,移除对用户服务的依赖
@Service
public class OrderService {
// 仅保留订单自身相关逻辑,不依赖UserService
public List<Order> getRecentOrders(Long userId) {
return orderRepository.findByUserIdOrderByCreateTimeDesc(userId, PageRequest.of(0, 5));
}
}
这种重构通过引入协调者模式(Coordinator)分离了交叉关注点,使 UserService 和 OrderService 恢复单向依赖关系,彻底消除循环引用。
注入方式调整可在不改变代码结构的情况下临时解决问题,适合过渡期使用:
@Service
public class OrderService {
// 字段注入(不推荐,但可临时解决循环依赖)
@Autowired
private UserService userService;
// 或使用setter注入
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
@Service
public class UserService {
// 使用ObjectProvider延迟注入
private final ObjectProvider<OrderService> orderServiceProvider;
public UserService(ObjectProvider<OrderService> orderServiceProvider) {
this.orderServiceProvider = orderServiceProvider;
}
public User getUserById(Long id) {
User user = userRepository.findById(id);
// 延迟获取Bean,避免初始化阶段依赖
List<Order> recentOrders = orderServiceProvider.getObject().getRecentOrders(id);
user.setRecentOrders(recentOrders);
return user;
}
}
ObjectProvider 是 Spring 4.3 引入的延迟注入工具,通过getObject()方法在实际需要时才获取依赖 Bean,避免了初始化阶段的循环引用。相比字段注入,它保留了构造函数注入的优势,同时支持延迟加载。
接口提取是另一种有效的解耦方式,通过定义抽象接口减少直接依赖:
// 1. 定义订单服务接口
public interface OrderOperations {
List<Order> getRecentOrders(Long userId);
}
// 2. 定义用户服务接口
public interface UserOperations {
User getUserById(Long id);
}
// 3. 实现类仅依赖接口
@Service
public class OrderService implements OrderOperations {
private final UserOperations userOperations;
public OrderService(UserOperations userOperations) {
this.userOperations = userOperations;
}
// 实现方法...
}
@Service
public class UserService implements UserOperations {
private final OrderOperations orderOperations;
public UserService(OrderOperations orderOperations) {
this.orderOperations = orderOperations;
}
// 实现方法...
}
这种方式通过面向接口编程降低了类之间的耦合度,即使存在循环依赖,也能通过接口隔离减少直接依赖带来的风险。
兼容策略:配置调整与高级容器特性
对于无法立即重构的遗留系统,SpringBoot 2.6.0 + 提供了兼容性配置和高级特性,可在可控范围内恢复循环依赖处理能力。
恢复循环依赖支持是最简单的兼容方案,通过配置参数重新启用容器的自动处理:
# application.yml
spring:
main:
allow-circular-references: true # 允许循环引用
或在启动类中配置:
@SpringBootApplication
public class AppApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(AppApplication.class);
// 允许循环引用
application.setAllowCircularReferences(true);
application.run(args);
}
}
但需注意,这只是临时解决方案,官方强烈建议在未来版本中通过重构消除循环依赖。启用该配置后,应密切关注以下风险:
- 事务代理可能失效,需通过@EnableTransactionManagement(proxyTargetClass = true)强制使用 CGLIB 代理
- AOP 切面可能无法正确织入提前暴露的 Bean
- 启动顺序敏感的 Bean 可能出现初始化异常
使用 @Lazy 注解可实现延迟初始化,使依赖 Bean 在首次使用时才创建,避免启动阶段的循环依赖:
@Service
public class OrderService {
private final UserService userService;
// 对依赖进行延迟初始化
public OrderService(@Lazy UserService userService) {
this.userService = userService;
}
}
@Service
public class UserService {
private final OrderService orderService;
// 对依赖进行延迟初始化
public UserService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
}
@Lazy 注解会导致 Spring 创建代理对象而非直接实例化目标 Bean,当首次调用代理对象的方法时,才会触发实际 Bean 的初始化。这种方式保持了构造函数注入的优势,同时通过延迟加载打破了初始化阶段的依赖闭环。
工厂 Bean 模式适合复杂场景,通过自定义工厂类控制 Bean 的创建时机:
// 定义订单服务工厂
public class OrderServiceFactory implements FactoryBean<OrderService> {
@Autowired
private ApplicationContext context;
@Override
public OrderService getObject() {
// 从容器中获取依赖Bean
UserService userService = context.getBean(UserService.class);
return new OrderService(userService);
}
@Override
public Class<?> getObjectType() {
return OrderService.class;
}
}
// 定义用户服务工厂
public class UserServiceFactory implements FactoryBean<UserService> {
@Autowired
private ApplicationContext context;
@Override
public UserService getObject() {
// 从容器中获取依赖Bean
OrderService orderService = context.getBean(OrderService.class);
return new UserService(orderService);
}
@Override
public Class<?> getObjectType() {
return UserService.class;
}
}
// 配置工厂Bean
@Configuration
public class AppConfig {
@Bean
public FactoryBean<OrderService> orderServiceFactory() {
return new OrderServiceFactory();
}
@Bean
public FactoryBean<UserService> userServiceFactory() {
return new UserServiceFactory();
}
}
FactoryBean 通过getObject()方法动态创建 Bean 实例,使 Spring 能在完全初始化依赖 Bean 后再创建当前 Bean,从而避免循环依赖。这种方式灵活性高,但增加了代码复杂度,适合无法通过简单注解解决的复杂场景。
某电商平台升级 SpringBoot 2.7.x 时,采用 "接口提取 +@Lazy" 组合方案处理了 37 处循环依赖,在保持服务可用性的同时,为后续重构奠定了基础。系统启动时间缩短了 18%,事务相关的生产问题下降了 65%。
SpringBoot 2.6.0 + 禁用循环依赖自动处理,本质上是在推动开发者编写更清晰的代码。循环依赖本身并非错误,但它往往是代码职责不清的信号。在实际开发中,应优先通过重构消除循环依赖,其次考虑使用 ObjectProvider、@Lazy等 Spring 特性,最后才选择恢复循环依赖支持的兼容方案。
解决循环依赖的过程,也是重新梳理系统架构的契机。通过明确模块边界、拆分大服务、引入领域事件(如 Spring 的 ApplicationEvent)等方式,不仅能消除循环依赖,更能提升系统的可维护性和扩展性。记住,Spring 容器的自动处理是便利而非银弹,优秀的代码设计才是系统稳定的根本保障。
更多推荐


所有评论(0)