Кръгови зависимости през пролетта

1. Какво представлява кръговата зависимост?

Това се случва, когато боб А зависи от друг боб Б, а боб Б зависи и от боб А:

Bean A → Bean B → Bean A

Разбира се, бихме могли да имаме и повече боб:

Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Какво се случва през пролетта

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

Bean A → Bean B → Bean C

Spring ще създаде боб C, след това ще създаде боб В (и ще инжектира боб С в него), след това ще създаде боб А (и ще инжектира боб Б в него).

Но когато има кръгова зависимост, 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; } }

Сега можем да напишем конфигурационен клас за тестовете, нека го наречем TestConfig , който определя базовия пакет за сканиране за компоненти. Да приемем, че нашите зърна са дефинирани в пакет „ com.baeldung.circulardependency “:

@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, за да инициализираме един от зърната мързеливо. Тоест: вместо да инициализира напълно боб, той ще създаде прокси, за да го инжектира в другия боб. Инжектираният боб ще бъде напълно създаден само когато е необходим за първи път.

За да опитате това с нашия код, можете да промените CircularDependencyA на следното:

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

Ако стартирате теста сега, ще видите, че този път грешката не се случва.

4.3. Използвайте инжектор за настройка / поле

Едно от най-популярните заобикалящи решения, а също и това, което предлага документацията на Spring, е използването на инжектиране на сетер.

Казано по-просто, ако промените начините на свързване на зърната, за да използвате инжектиране на сетер (или инжектиране на поле) вместо инжектиране на конструктор - това решава проблема. По този начин Spring създава зърната, но зависимостите не се инжектират, докато не са необходими.

Нека направим това - нека променим нашите класове, за да използваме инжекции на сетер и ще добавим друго поле ( съобщение ) към 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 framework, че тези методи трябва да се използват за извличане на изпълнение на зърната, които да се инжектират.

@Test : Тестът ще получи CircularDependencyA боб от контекста и ще твърди, че CircularDependencyB е инжектиран правилно, като проверява стойността на свойството на съобщението .

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.