← Назад к вопросам

Сталкивался ли с циклическими зависимостями бинов в Spring

1.8 Middle🔥 221 комментариев
#Docker, Kubernetes и DevOps

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Циклические зависимости в Spring: проблема и решения

Да, я сталкивался с циклическими зависимостями между бинами в Spring и это была полезная ошибка для глубокого понимания того, как работает контейнер зависимостей.

Пример циклической зависимости

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

Эта конфигурация приведёт к ошибке:

BeanCurrentlyInCreationException: Error creating bean with name serviceA:
Requested bean is currently in creation: Is there an unresolvable circular reference?

Как Spring создаёт бины

Spring использует трёхуровневую систему кэширования для обработки циклических зависимостей:

  1. singletonObjects — готовые бины (level 1)
  2. earlySingletonObjects — бины в процессе создания (level 2)
  3. singletonFactories — фабрики для создания бинов (level 3)
// Упрощённо, как работает механизм:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
private final Map<String, Object> earlySingletonObjects = new HashMap<>();
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

Это позволяет обработать циклические зависимости при инъекции через поле (field injection).

Способ 1: Инъекция через поле (Field Injection)

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    public void doWork() {
        serviceB.process();
    }
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
    
    public void process() {
        // ...
    }
}

Это работает благодаря earlySingletonObjects, но это не лучшая практика, так как скрывает зависимости.

Способ 2: Использование ObjectProvider (ПРАВИЛЬНЫЙ ПОДХОД)

@Component
public class ServiceA {
    private final ObjectProvider<ServiceB> serviceBProvider;
    
    public ServiceA(ObjectProvider<ServiceB> serviceBProvider) {
        this.serviceBProvider = serviceBProvider;
    }
    
    public void doWork() {
        ServiceB serviceB = serviceBProvider.getIfAvailable();
        if (serviceB != null) {
            serviceB.process();
        }
    }
}

@Component
public class ServiceB {
    private final ObjectProvider<ServiceA> serviceAProvider;
    
    public ServiceB(ObjectProvider<ServiceA> serviceAProvider) {
        this.serviceAProvider = serviceAProvider;
    }
    
    public void process() {
        // ...
    }
}

Способ 3: Использование @Lazy

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(@Lazy ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

Способ 4: Рефакторинг архитектуры

Делим ответственность на новый сервис:

@Component
public class ServiceA {
    private final Coordinator coordinator;
    
    public ServiceA(Coordinator coordinator) {
        this.coordinator = coordinator;
    }
}

@Component
public class ServiceB {
    private final Coordinator coordinator;
    
    public ServiceB(Coordinator coordinator) {
        this.coordinator = coordinator;
    }
}

@Component
public class Coordinator {
    private final ServiceA serviceA;
    private final ServiceB serviceB;
    
    public Coordinator(ServiceA serviceA, ServiceB serviceB) {
        this.serviceA = serviceA;
        this.serviceB = serviceB;
    }
}

Решение, которое я применял

Мой практический подход:

  1. Первое — пересмотреть дизайн. Циклические зависимости обычно признак плохой архитектуры.
  2. Второе — использовать ObjectProvider для ленивого разрешения зависимостей.
  3. Третье — если нужно, применить паттерн Event-Driven Architecture.

Пример с Events

@Component
public class ServiceA {
    private final ApplicationEventPublisher eventPublisher;
    
    public ServiceA(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void doWork() {
        eventPublisher.publishEvent(new WorkEvent(this));
    }
}

@Component
public class ServiceB {
    @EventListener
    public void onWorkEvent(WorkEvent event) {
        // Реагируем на событие без прямой зависимости
    }
}

Ключевые выводы

  • Избегайте циклических зависимостей на уровне архитектуры
  • Используйте ObjectProvider вместо прямой инъекции в сложных случаях
  • @Lazy помогает, но замаскирует проблему
  • Event-driven архитектура часто лучший способ развязать компоненты