在 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 之前,这段代码能正常启动,因为容器通过以下三级缓存机制处理:

  1. 一级缓存(singletonObjects):存储完全初始化的 Bean
  1. 二级缓存(earlySingletonObjects):存储提前暴露的原始 Bean
  1. 三级缓存(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 容器的自动处理是便利而非银弹,优秀的代码设计才是系统稳定的根本保障。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐