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

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

1. Что такое круговая зависимость?

Это происходит, когда компонент A зависит от другого компонента B, а компонент B также зависит от компонента A:

Бин A → Бин B → Бин A

Конечно, мы могли бы иметь в виду больше бобов:

Бин A → Бин B → Бин C → Бин D → Бин E → Бин A

2. Что происходит весной

Когда контекст Spring загружает все компоненты, он пытается создать компоненты в порядке, необходимом для их полной работы. Например, если у нас не было циклической зависимости, как в следующем случае:

Бин A → Бин B → Бин C

Spring создаст bean-компонент C, затем создаст bean-компонент B (и внедрит в него bean-компонент C), затем создаст bean-компонент A (и добавит bean-компонент B в него).

Но, имея циклическую зависимость, Spring не может решить, какой из компонентов должен быть создан первым, так как они зависят друг от друга. В этих случаях Spring подниметBeanCurrentlyInCreationException при загрузке контекста.

Это может произойти в Spring при использованииconstructor injection; Если вы используете другие типы инъекций, вы не должны столкнуться с этой проблемой, поскольку зависимости будут внедряться, когда они необходимы, а не при загрузке контекста.

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, который указывает базовый пакет для сканирования компонентов. Предположим, наши beans определены в пакете «com.example.circulardependency»:

@Configuration
@ComponentScan(basePackages = { "com.example.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, чтобы лениво инициализировать один из компонентов. То есть: вместо полной инициализации компонента он создает прокси-сервер для внедрения его в другой компонент. Впрыснутый боб будет полностью создан только тогда, когда это будет необходимо.

Чтобы попробовать это с вашим кодом, вы можете изменить Круговую зависимость на следующую:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

Если вы запустите тест сейчас, вы увидите, что ошибка не произойдет в этот раз.

4.3. Использовать сеттер / ввод поля

Один из самых популярных обходных путей, а такжеSpring documentation proposes - это внедрение установщика.

Проще говоря, если вы измените способ, которым ваши bean-компоненты подключены, чтобы использовать инъекцию сеттера (или полевую инъекцию) вместо инжектора конструктора - это решает проблему. Таким образом 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());
    }
}

Ниже поясняются аннотации, показанные выше:

@Bean: чтобы сообщить фреймворку Spring, что эти методы должны использоваться для получения реализации bean-компонентов для внедрения.

@Test: тест получит bean-компонент CircularDependencyA из контекста и подтвердит, что его CircularDependencyB был введен правильно, проверяя значение его свойстваmessage.

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

Если один из bean-компонентов реализуетApplicationContextAware, он имеет доступ к контексту Spring и может извлекать оттуда другой bean-компонент. Реализуя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 project.