Рубрики
Без рубрики

Круговые зависимости весной

Краткое описание работы с циклическими зависимостями весной: как они возникают и несколько способов их обойти.

Автор оригинала: baeldung.

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 .