1. Что такое Циклическая зависимость?
Это происходит, когда боб A зависит от другого боба B, и боб B также зависит от боба A:
Been A = Bean B = Bean A
Конечно, мы могли бы иметь больше бобов.:
Been A = Bean B = Bean C = Be и = BeanE = Bean A
2. Что происходит весной
Когда Spring context загружает все бобы, он пытается создать бобы в порядке, необходимом для их полной работы. Например, если бы у нас не было циклической зависимости, как в следующем случае:
Был A = Боб B = Боб C
Spring создаст bean C, затем создаст bean B (и введет в него bean Cachedconnectionmanager), затем создаст bean A (и введет в него bean B).
Но, имея циклическую зависимость, Spring не может решить, какой из бобов должен быть создан первым, так как они зависят друг от друга. В этих случаях Spring вызовет исключение BeanCurrentlyInCreationException при загрузке контекста.
Это может произойти весной при использовании инъекции конструктора ; если вы используете другие типы инъекций, вы не должны обнаруживать эту проблему, так как зависимости будут вводиться, когда они необходимы, а не при загрузке контекста.
3. Краткий Пример
Давайте определим два компонента, которые зависят друг от друга (с помощью инъекции конструктора):
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(CircularDependencyB circB) {
this.circB = circB;
}
}@Component
public class CircularDependencyB {
private CircularDependencyA circA;
@Autowired
public CircularDependencyB(CircularDependencyA circA) {
this.circA = circA;
}
}Теперь мы можем написать класс конфигурации для тестов, назовем его Test Config , который определяет базовый пакет для сканирования компонентов. Предположим, что наши бобы определены в пакете ” com.baeldung.circular dependence “:
@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}И, наконец, мы можем написать тест JUnit для проверки циклической зависимости. Тест может быть пустым, так как циклическая зависимость будет обнаружена во время загрузки контекста.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
@Test
public void givenCircularDependency_whenConstructorInjection_thenItFails() {
// Empty test; we just want the context to load
}
}Если вы попытаетесь запустить этот тест, вы получите следующее исключение:
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?
4. Обходные пути
Мы покажем некоторые из наиболее популярных способов решения этой проблемы.
4.1. Редизайн
Когда у вас есть циклическая зависимость, скорее всего, у вас есть проблема с дизайном, и обязанности не очень хорошо разделены. Вы должны попытаться правильно перепроектировать компоненты, чтобы их иерархия была хорошо продумана и не было необходимости в циклических зависимостях.
Если вы не можете перепроектировать компоненты (для этого может быть много возможных причин: устаревший код, код, который уже был протестирован и не может быть изменен, недостаточно времени или ресурсов для полного перепроектирования…), можно попробовать некоторые обходные пути.
4.2. Используйте @Lazy
Простой способ разорвать цикл-сказать Spring, чтобы лениво инициализировать один из бобов. То есть: вместо полной инициализации компонента он создаст прокси-сервер, чтобы ввести его в другой компонент. Введенный боб будет полностью создан только тогда, когда он впервые понадобится.
Чтобы попробовать это с вашим кодом, вы можете изменить циклическую зависимость A на следующую:
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}Если вы запустите тест сейчас, вы увидите, что на этот раз ошибки не произойдет.
4.3. Используйте Инъекцию Сеттера/Поля
Одним из самых популярных обходных путей, а также то , что предлагает документация Spring , является использование инъекции сеттера.
Проще говоря, если вы измените способы подключения ваших бобов, чтобы использовать инъекцию сеттера (или инъекцию поля) вместо инъекции конструктора – это решит проблему. Таким образом, Spring создает бобы, но зависимости не вводятся до тех пор, пока они не понадобятся.
Давайте сделаем это – давайте изменим наши классы на использование инъекций сеттера и добавим еще одно поле ( message ) в CircularDependencyB , чтобы мы могли сделать правильный модульный тест:
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public void setCircB(CircularDependencyB circB) {
this.circB = circB;
}
public CircularDependencyB getCircB() {
return circB;
}
}@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}Теперь нам нужно внести некоторые изменения в наш модульный тест:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
@Autowired
ApplicationContext context;
@Bean
public CircularDependencyA getCircularDependencyA() {
return new CircularDependencyA();
}
@Bean
public CircularDependencyB getCircularDependencyB() {
return new CircularDependencyB();
}
@Test
public void givenCircularDependency_whenSetterInjection_thenItWorks() {
CircularDependencyA circA = context.getBean(CircularDependencyA.class);
Assert.assertEquals("Hi!", circA.getCircB().getMessage());
}
}Ниже приведены пояснения, приведенные выше:
@@Been : Чтобы сообщить Spring framework, что эти методы должны использоваться для извлечения реализации компонентов для инъекции.
@@Test : Тест получит компонент циклической зависимости A из контекста и подтвердит, что его CircularDependencyB был введен правильно, проверив значение его свойства message .
4.4. Используйте @PostConstruct
Другой способ разорвать цикл-это ввести зависимость с помощью @Autowired на одном из компонентов, а затем используйте метод с аннотацией @PostConstruct для установки другой зависимости.
Наши бобы могут иметь следующий код:
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
@PostConstruct
public void init() {
circB.setCircA(this);
}
public CircularDependencyB getCircB() {
return circB;
}
}@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}И мы можем запустить тот же тест, что и ранее, поэтому мы проверяем, что исключение циклической зависимости по-прежнему не генерируется и что зависимости правильно введены.
4.5. Реализация ApplicationContextAware и InitializingBean
Если один из компонентов реализует ApplicationContextAware , компонент имеет доступ к контексту Spring и может извлечь оттуда другой компонент. Реализация InitializingBean мы указываем, что этот компонент должен выполнить некоторые действия после того, как все его свойства были заданы; в этом случае мы хотим вручную установить нашу зависимость.
Код наших бобов был бы:
@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {
private CircularDependencyB circB;
private ApplicationContext context;
public CircularDependencyB getCircB() {
return circB;
}
@Override
public void afterPropertiesSet() throws Exception {
circB = context.getBean(CircularDependencyB.class);
}
@Override
public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
context = ctx;
}
}@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}Опять же, мы можем запустить предыдущий тест и убедиться, что исключение не вызвано и что тест работает должным образом.
5. В Заключение
Есть много способов справиться с циклическими зависимостями весной. Первое, что следует рассмотреть, – это перепроектировать свои компоненты, чтобы не было необходимости в циклических зависимостях: они обычно являются признаком дизайна, который можно улучшить.
Но если вам абсолютно необходимо иметь циклические зависимости в вашем проекте, вы можете следовать некоторым предложенным здесь обходным путям.
Предпочтительным методом является использование инъекций сеттера. Но есть и другие альтернативы, как правило, основанные на том, чтобы остановить Spring от управления инициализацией и инъекцией компонентов, и сделать это самостоятельно, используя ту или иную стратегию.
Примеры можно найти в проекте GitHub .