Как реализовать циклическую зависимость бинов в Spring и как её избежать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализовать циклическую зависимость бинов в 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();
}
}
Как это работает:
- Spring создаёт пустой ServiceA (конструктор без параметров)
- Spring создаёт пустой ServiceB
- Spring вызывает setServiceB(B) на A
- Spring вызывает setServiceA(A) на B
- Всё работает!
Минусы: нарушается инкапсуляция, зависимости не явные.
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
- Предпочитайте конструкторную инъекцию (явные зависимости)
- Избегайте циклических зависимостей через переосмысление архитектуры
- Используйте ObjectProvider<T> для отложенного связывания
- Разделяйте ответственность между сервисами
- Event-Driven для слабой связанности
- Interfaces для разделения зависимостей
Циклические зависимости — это красный флаг, указывающий на проблемы в дизайне. Вместо того чтобы их обходить, лучше переосмыслить архитектуру приложения.