Перейти к основному содержимому

Циклические зависимости в Spring

· 7 мин. чтения

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 .