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

Что будет при инъекции бина А в бин В, а бина В в бин А?

1.7 Middle🔥 111 комментариев
#SOLID и паттерны проектирования#Spring Framework

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

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

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

Циклические зависимости в Spring (Circular Dependency)

Это классический вопрос на собеседовании, и я сталкивался с этим не раз в real-world проектах. Когда бин А зависит от бина В, а бин В зависит от бина А, возникает циклическая зависимость.

Что происходит?

Spring контейнер обнаруживает эту циклическую зависимость и по умолчанию автоматически её разрешает, но поведение зависит от типа инъекции.

Scenario 1: Constructor Injection (Инъекция через конструктор)

Это самый опасный случай, который приводит к ошибке при старте приложения.

@Component
public class BeanA {
    private final BeanB beanB;
    
    // Constructor injection - циклическая зависимость!
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

@Component
public class BeanB {
    private final BeanA beanA;
    
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}

Результат: BeanCurrentlyInCreationException

The dependencies of some of the beans in the application context form a cycle:
  beanA -> beanB -> beanA

Почему так происходит?

  • Spring пытается создать BeanA
  • Для BeanA нужен BeanB (требуется конструктор)
  • Для BeanB нужен BeanA (требуется конструктор)
  • Бесконечный цикл - Spring выбрасывает исключение

Scenario 2: Setter Injection (Инъекция через setter)

Это работает благодаря двухфазному созданию объектов.

@Component
public class BeanA {
    private BeanB beanB;
    
    @Autowired
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }
}

@Component
public class BeanB {
    private BeanA beanA;
    
    @Autowired
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

Результат: Приложение стартует успешно!

Почему это работает:

  1. Spring создаёт BeanA без зависимостей (вызывает конструктор)
  2. Spring создаёт BeanB без зависимостей (вызывает конструктор)
  3. Затем Spring вызывает setters и устанавливает зависимости
  4. Циклическая зависимость разрешена через setters

Scenario 3: Field Injection (@Autowired на поле)

Работает аналогично setter injection.

@Component
public class BeanA {
    @Autowired
    private BeanB beanB;  // Инъекция после создания
}

@Component
public class BeanB {
    @Autowired
    private BeanA beanA;  // Инъекция после создания
}

Результат: Приложение стартует успешно!

Но это bad practice по разным причинам.

Как разрешить циклическую зависимость?

Вариант 1: Использовать ObjectProvider (рекомендуется)

@Component
public class BeanA {
    private final ObjectProvider<BeanB> beanBProvider;
    
    public BeanA(ObjectProvider<BeanB> beanBProvider) {
        this.beanBProvider = beanBProvider;  // Lazy resolution
    }
    
    public void doSomething() {
        BeanB beanB = beanBProvider.getIfAvailable();
        // Используем beanB
    }
}

Преимущества:

  • Работает с constructor injection
  • Lazy loading - зависимость разрешается позже
  • Type-safe

Вариант 2: Использовать Lazy

@Component
public class BeanA {
    private final Lazy<BeanB> beanB;
    
    public BeanA(Lazy<BeanB> beanB) {
        this.beanB = beanB;
    }
    
    public void work() {
        BeanB actual = beanB.get();  // Получить при необходимости
    }
}

Вариант 3: Рефакторинг дизайна (Best Practice)

Если у вас циклическая зависимость - это часто признак того, что архитектура неправильная.

// Выделить общий интерфейс
public interface Service {
    void process();
}

@Component
public class ServiceA implements Service {
    // Не зависит от ServiceB
}

@Component
public class ServiceB {
    private final Service service;  // Зависит от интерфейса
    
    public ServiceB(Service service) {
        this.service = service;
    }
}

Мой подход в production

В настоящих проектах я избегаю циклических зависимостей:

  1. Первый выбор: Рефакторинг архитектуры
  2. Второй выбор: ObjectProvider для ленивого разрешения
  3. Третий выбор: EventPublisher pattern для декаплинга

Event-driven подход:

@Component
public class BeanA {
    private final ApplicationEventPublisher publisher;
    
    public BeanA(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    
    public void notify() {
        publisher.publishEvent(new MyEvent());  // Не зависит от BeanB
    }
}

@Component
public class BeanB {
    @EventListener
    public void onEvent(MyEvent event) {
        // Слушаем события от BeanA
    }
}

Этот подход полностью устраняет циклическую зависимость и делает компоненты более независимыми.