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

Как реализовать циклическую зависимость бинов в Spring и как её избежать?

2.0 Middle🔥 231 комментариев
#Spring Framework

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

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

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

Как реализовать циклическую зависимость бинов в Spring и как её избежать?

Циклическая зависимость — это проблема, когда бин A зависит от бина B, а бин B зависит от бина A. Spring обычно обнаруживает это и выбрасывает исключение при запуске приложения. Понимание причин и способов решения этой проблемы — критически важно для работы со Spring.

1. Что такое циклическая зависимость

// Сервис A
@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;  // зависит от B
    }
}

// Сервис B
@Service
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;  // зависит от A
    }
}

Результат: BeanCurrentlyInCreationException при старте Spring.

2. Почему Spring это обнаруживает

Spring использует конструкторную инъекцию по умолчанию. При создании бина она пытается:

1. Создать ServiceA
   → Нужен ServiceB
   → Создавать ServiceB
      → Нужен ServiceA
      → Но ServiceA уже создаётся!
      → ОШИБКА: циклическая зависимость

Однако Spring имеет механизм отложенного связывания для некоторых сценариев.

3. Способ 1: Использовать сеттер-инъекцию (обойти через @Autowired)

Это ОДИН из способов, который работает благодаря двухфазной инициализации Spring:

@Service
public class ServiceA {
    private ServiceB serviceB;
    
    // Конструктор БЕЗ зависимостей
    public ServiceA() {
    }
    
    // Сеттер с @Autowired (вызывается после конструктора)
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
    
    public void doSomething() {
        serviceB.process();
    }
}

@Service
public class ServiceB {
    private ServiceA serviceA;
    
    public ServiceB() {
    }
    
    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
    
    public void process() {
        serviceA.doSomething();
    }
}

Как это работает:

  1. Spring создаёт пустой ServiceA (конструктор без параметров)
  2. Spring создаёт пустой ServiceB
  3. Spring вызывает setServiceB(B) на A
  4. Spring вызывает setServiceA(A) на B
  5. Всё работает!

Минусы: нарушается инкапсуляция, зависимости не явные.

4. Способ 2: ObjectProvider и ленивое связывание

Это рекомендуемый способ — используйте ObjectProvider или Lazy:

@Service
public class ServiceA {
    private final ObjectProvider<ServiceB> serviceB;
    
    public ServiceA(ObjectProvider<ServiceB> serviceB) {
        this.serviceB = serviceB;  // Не требует ServiceB при создании!
    }
    
    public void doSomething() {
        // Получаем ServiceB только когда он нужен
        serviceB.getIfAvailable(this::handleIfUnavailable);
    }
    
    private void handleIfUnavailable() {
        System.out.println("ServiceB недоступен");
    }
}

@Service
public class ServiceB {
    private final ObjectProvider<ServiceA> serviceA;
    
    public ServiceB(ObjectProvider<ServiceA> serviceA) {
        this.serviceA = serviceA;
    }
    
    public void process() {
        serviceA.ifAvailable(a -> a.doSomething());
    }
}

Или через Lazy:

@Service
public class ServiceA {
    private final Lazy<ServiceB> serviceB;
    
    public ServiceA(Lazy<ServiceB> serviceB) {
        this.serviceB = serviceB;
    }
    
    public void doSomething() {
        serviceB.get().process();  // Получаем только при вызове
    }
}

@Service
public class ServiceB {
    private final Lazy<ServiceA> serviceA;
    
    public ServiceB(Lazy<ServiceA> serviceA) {
        this.serviceA = serviceA;
    }
    
    public void process() {
        serviceA.get().doSomething();
    }
}

Плюсы: безопасно, явное, ленивое вычисление.

5. Способ 3: ApplicationContext (полная ленивость)

Получайте другой сервис динамически при необходимости:

@Service
public class ServiceA {
    private final ApplicationContext context;
    
    public ServiceA(ApplicationContext context) {
        this.context = context;
    }
    
    public void doSomething() {
        // Получаем ServiceB только когда нужен
        ServiceB serviceB = context.getBean(ServiceB.class);
        serviceB.process();
    }
}

@Service
public class ServiceB {
    private final ApplicationContext context;
    
    public ServiceB(ApplicationContext context) {
        this.context = context;
    }
    
    public void process() {
        ServiceA serviceA = context.getBean(ServiceA.class);
        serviceA.doSomething();
    }
}

Плюсы: полная гибкость, избегает циклических зависимостей. Минусы: менее явно, может скрыть проблемы в дизайне.

6. Способ 4: Разделить на две зависимости

Это лучший способ — переосмыслить архитектуру:

// Интерфейс для разделения зависимостей
public interface ServiceBContract {
    void process();
}

public interface ServiceAContract {
    void doSomething();
}

// Реализация A
@Service
public class ServiceA implements ServiceAContract {
    private final ServiceBContract serviceB;
    
    public ServiceA(ServiceBContract serviceB) {
        this.serviceB = serviceB;
    }
    
    @Override
    public void doSomething() {
        serviceB.process();
    }
}

// Реализация B
@Service
public class ServiceB implements ServiceBContract {
    private final ServiceAContract serviceA;
    
    public ServiceB(ServiceAContract serviceA) {
        this.serviceA = serviceA;
    }
    
    @Override
    public void process() {
        // Используем контракт, а не конкретный класс
        serviceA.doSomething();
    }
}

Нет циклической зависимости, так как зависимости идут на интерфейсы, а не на конкретные классы.

7. Способ 5: Медиатор (Event-Driven)

Используйте события для обхода циклических зависимостей:

// Событие
public class ProcessEvent {
    private final String data;
    
    public ProcessEvent(String data) {
        this.data = data;
    }
    
    public String getData() { return data; }
}

// Сервис A публикует события
@Service
public class ServiceA {
    private final ApplicationEventPublisher eventPublisher;
    
    public ServiceA(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void doSomething() {
        System.out.println("ServiceA работает");
        eventPublisher.publishEvent(new ProcessEvent("данные"));
    }
}

// Сервис B слушает события
@Service
public class ServiceB {
    @EventListener
    public void onProcessEvent(ProcessEvent event) {
        System.out.println("ServiceB обработал: " + event.getData());
    }
}

Без циклических зависимостей! Сервисы слабо связаны.

8. Практический пример: Repository и Service

Обычная (но неправильная) циклическая зависимость:

// ПЛОХО
@Service
public class UserService {
    private final UserRepository repository;
    private final UserValidator validator;
    
    public UserService(UserRepository repository, UserValidator validator) {
        this.repository = repository;
        this.validator = validator;
    }
}

@Service
public class UserValidator {
    private final UserService service;  // ЦИКЛИЧЕСКАЯ ЗАВИСИМОСТЬ!
    
    public UserValidator(UserService service) {
        this.service = service;
    }
}

Хорошее решение:

@Service
public class UserService {
    private final UserRepository repository;
    private final UserValidator validator;
    
    public UserService(UserRepository repository, UserValidator validator) {
        this.repository = repository;
        this.validator = validator;
    }
    
    public void createUser(User user) {
        validator.validate(user);  // Используем validator
        repository.save(user);
    }
}

@Service
public class UserValidator {
    private final UserRepository repository;  // Зависит только от repository
    
    public UserValidator(UserRepository repository) {
        this.repository = repository;
    }
    
    public void validate(User user) {
        if (repository.exists(user.getEmail())) {
            throw new ValidationException("Email уже существует");
        }
    }
}

Теперь нет циклических зависимостей!

9. Как обнаружить циклические зависимости

На логах Spring:

The dependencies of some of the beans in the application context form a cycle:

┌─────────────────┐
|     ServiceA    |
└────────┬────────┘
         │
         ↓
┌─────────────────┐
|     ServiceB    |
└────────┬────────┘
         │
         ↓
┌─────────────────┐
|     ServiceA    |
└─────────────────┘

10. Best Practices

  1. Предпочитайте конструкторную инъекцию (явные зависимости)
  2. Избегайте циклических зависимостей через переосмысление архитектуры
  3. Используйте ObjectProvider<T> для отложенного связывания
  4. Разделяйте ответственность между сервисами
  5. Event-Driven для слабой связанности
  6. Interfaces для разделения зависимостей

Циклические зависимости — это красный флаг, указывающий на проблемы в дизайне. Вместо того чтобы их обходить, лучше переосмыслить архитектуру приложения.

Как реализовать циклическую зависимость бинов в Spring и как её избежать? | PrepBro