Круговые зависимости весной
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.