1. Что такое циклическая зависимость?
Циклическая зависимость возникает, когда компонент A зависит от другого компонента B, а компонент B также зависит от компонента A:
Боб A → Боб B → Боб A
Конечно, мы могли бы иметь больше bean-компонентов:
Боб A → Боб B → Боб C → Боб D → Боб E → Боб A
2. Что происходит весной
Когда контекст Spring загружает все bean-компоненты, он пытается создать bean-компоненты в порядке, необходимом для их полной работы.
Допустим, у нас нет циклической зависимости. Вместо этого у нас есть что-то вроде этого:
Боб A → Боб B → Боб C
Spring создаст bean-компонент C, затем создаст bean-компонент B (и внедрит в него bean-компонент C), затем создаст bean-компонент A (и введет в него bean-компонент B).
Но с круговой зависимостью Spring не может решить, какой из bean-компонентов должен быть создан первым, поскольку они зависят друг от друга. В этих случаях Spring вызовет исключение BeanCurrentlyInCreationException
при загрузке контекста.
Это может произойти в Spring при использовании внедрения конструктора. Если мы используем другие типы инъекций, у нас не должно возникнуть этой проблемы, поскольку зависимости будут инжектироваться тогда, когда они необходимы, а не при загрузке контекста.
3. Краткий пример
Давайте определим два bean-компонента, которые зависят друг от друга (через внедрение конструктора):
@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;
}
}
Теперь мы можем написать класс конфигурации для тестов (назовем его TestConfig
), который указывает базовый пакет для сканирования компонентов.
Предположим, что наши bean-компоненты определены в пакете « com.foreach.circulardependency
»:
@Configuration
@ComponentScan(basePackages = { "com.foreach.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. Используйте @Ленивый
Простой способ разорвать цикл — указать Spring лениво инициализировать один из bean-компонентов. Таким образом, вместо полной инициализации bean-компонента будет создан прокси-сервер для его внедрения в другой bean-компонент. Внедренный bean-компонент будет полностью создан только тогда, когда он впервые понадобится.
Чтобы попробовать это с нашим кодом, мы можем изменить CircularDependencyA
:
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
Если мы сейчас запустим тест, то увидим, что на этот раз ошибки не возникает.
4.3. Использовать сеттер/внедрение поля
Одним из самых популярных обходных путей, а также тем, что предлагает документация Spring , является использование внедрения сеттера.
Проще говоря, мы можем решить проблему, изменив способы подключения наших bean-компонентов — использовать внедрение сеттера (или внедрение поля) вместо внедрения конструктора. Таким образом, Spring создает bean-компоненты, но зависимости не внедряются до тех пор, пока они не потребуются.
Итак, давайте изменим наши классы, чтобы они использовали инъекции сеттера, и добавим еще одно поле ( сообщение
) в 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());
}
}
Давайте подробнее рассмотрим эти аннотации.
@Bean
сообщает платформе Spring, что эти методы должны использоваться для получения реализации bean-компонентов для внедрения.
А с аннотацией @Test
тест получит bean-компонент CircularDependencyA
из контекста и подтвердит, что его CircularDependencyB
был введен правильно, проверяя значение его свойства сообщения .
4.4. Используйте @PostConstruct
Другой способ разорвать цикл — внедрить зависимость с помощью @Autowired
в один из bean-компонентов, а затем использовать метод, аннотированный с помощью @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 есть много способов справиться с циклическими зависимостями.
Сначала мы должны подумать о перепроектировании наших bean-компонентов, чтобы не было необходимости в циклических зависимостях. Это потому, что круговые зависимости обычно являются признаком дизайна, который можно улучшить.
Но если нам абсолютно необходимы циклические зависимости в нашем проекте, мы можем воспользоваться некоторыми обходными путями, предложенными здесь.
Предпочтительным методом является использование сеттерных инъекций. Но есть и другие альтернативы, обычно основанные на запрете Spring управлять инициализацией и внедрением bean-компонентов, а также на выполнении этого самостоятельно с использованием различных стратегий.
Примеры в этой статье можно найти в проекте GitHub .