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 .