Если вы используете Spring Framework больше недели, вы, вероятно, знаете об этой функции. Предположим, у вас есть более одного компонента, реализующего данный интерфейс. Попытка автоматического подключения только одного компонента такого интерфейса обречена на неудачу, потому что Spring не знает, какой именно экземпляр вам нужен. Вы можете обойти это, используя аннотацию @Primary чтобы обозначить ровно одну « самую важную » реализацию, которая будет иметь приоритет над другими. Но есть много законных вариантов использования, когда вы хотите внедрить все bean-компоненты, реализующие указанный интерфейс. Например, у вас есть несколько валидаторов, которые все должны быть выполнены до бизнес-логики, или несколько реализаций алгоритмов, которые вы хотите использовать одновременно. Автоматическое обнаружение всех реализаций во время выполнения является фантастической иллюстрацией принципа « открытый / закрытый» : вы можете легко добавить новое поведение в бизнес-логику (валидаторы, алгоритмы, стратегии — открытые для расширения), не касаясь самой бизнес-логики ( закрытой для модификации).
На всякий случай я начну с краткого вступления, не стесняйтесь сразу переходить к следующим разделам. Итак, давайте возьмем конкретный пример. Представьте, что у вас есть интерфейс StringCallable и несколько реализаций:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
interface StringCallable extends Callable<String> { } @Componentclass Third implements StringCallable { @Override public String call() { return "3"; } } @Componentclass Forth implements StringCallable { @Override public String call() { return "4"; } } @Componentclass Fifth implements StringCallable { @Override public String call() throws Exception { return "5"; }} |
Теперь мы можем List<StringCallable> , Set<StringCallable> или даже Map<String, StringCallable> ( String представляет имя компонента) в любой другой класс. Для упрощения я делаю инъекцию в тестовый пример:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
@SpringBootApplication public class Bootstrap { } @ContextConfiguration(classes = Bootstrap)class BootstrapTest extends Specification { @Autowired List<StringCallable> list; @Autowired Set<StringCallable> set; @Autowired Map<String, StringCallable> map; def 'injecting all instances of StringCallable'() { expect: list.size() == 3 set.size() == 3 map.keySet() == ['third', 'forth', 'fifth'].toSet() } def 'enforcing order of injected beans in List'() { when: def result = list.collect { it.call() } then: result == ['3', '4', '5'] } def 'enforcing order of injected beans in Set'() { when: def result = set.collect { it.call() } then: result == ['3', '4', '5'] } def 'enforcing order of injected beans in Map'() { when: def result = map.values().collect { it.call() } then: result == ['3', '4', '5'] } } |
Пока все хорошо, но только первые тесты, вы можете догадаться, почему?
|
1
2
3
4
5
6
|
Condition not satisfied: result == ['3', '4', '5']| || false[3, 5, 4] |
В конце концов, почему мы предположили, что бобы будут вводиться в том же порядке, в каком они были … объявлены? В алфавитном порядке? К счастью, можно применить порядок с помощью интерфейса Ordered :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
interface StringCallable extends Callable<String>, Ordered {} @Componentclass Third implements StringCallable { //... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }} @Componentclass Forth implements StringCallable { //... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1; }} @Componentclass Fifth implements StringCallable { //... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 2; }} |
Интересно, что хотя Spring внутренне внедряет LinkedHashMap и LinkedHashSet , только List правильно упорядочен. Я думаю, это не задокументировано и наименее удивительно. Чтобы завершить это введение, в Java 8 вы также можете внедрить Optional<MyService> который работает как положено: внедряет зависимость, только если она доступна. Могут появляться необязательные зависимости, например, при интенсивном использовании профилей, а некоторые компоненты не загружаются в некоторые профили.
Композитный узор
Работа со списками довольно громоздка. Большую часть времени вы хотите перебирать их, поэтому во избежание дублирования полезно инкапсулировать такой список в специальную оболочку:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Componentpublic class Caller { private final List<StringCallable> callables; @Autowired public Caller(List<StringCallable> callables) { this.callables = callables; } public String doWork() { return callables.stream() .map(StringCallable::call) .collect(joining("|")); } } |
Наша оболочка просто вызывает все лежащие в основе вызываемые объекты один за другим и объединяет их результаты:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@ContextConfiguration(classes = Bootstrap)class CallerTest extends Specification { @Autowired Caller caller def 'Caller should invoke all StringCallbles'() { when: def result = caller.doWork() then: result == '3|4|5' } } |
Это несколько спорно, но часто эта обертка реализует тот же интерфейс , а также, эффективной реализация композитного классического шаблона проектирования:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
@Component@Primarypublic class Caller implements StringCallable { private final List<StringCallable> callables; @Autowired public Caller(List<StringCallable> callables) { this.callables = callables; } @Override public String call() { return callables.stream() .map(StringCallable::call) .collect(joining("|")); } } |
Благодаря @Primary мы можем просто автоматически связывать StringCallable везде, как если бы был только один компонент, хотя на самом деле их несколько, и мы вводим составные. Это полезно при рефакторинге старого приложения, поскольку сохраняет обратную совместимость.
Почему я даже начинаю со всех этих основ? Если вы посмотрите очень внимательно, приведенный выше фрагмент кода представляет проблему курицы и яйца: для экземпляра StringCallable требуются все экземпляры StringCallable , поэтому в техническом плане список callables StringCallable должен включать Caller . Но Caller в настоящее время создается, поэтому это невозможно. Это имеет большой смысл, и, к счастью, Spring признает этот особый случай. Но в более сложных сценариях это может вас укусить. В дальнейшем новый разработчик представил это :
|
01
02
03
04
05
06
07
08
09
10
|
@Componentpublic class EnterpriseyManagerFactoryProxyHelperDispatcher { private final Caller caller; @Autowired public EnterpriseyManagerFactoryProxyHelperDispatcher(Caller caller) { this.caller = caller; }} |
Пока ничего плохого, кроме имени класса. Но что произойдет, если один из StringCallables зависит от него?
|
01
02
03
04
05
06
07
08
09
10
11
|
@Componentclass Fifth implements StringCallable { private final EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher; @Autowired public Fifth(EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher) { this.dispatcher = dispatcher; } } |
Теперь мы создали циклическую зависимость, и поскольку мы внедряем ее через конструкторы (как это и должно было быть), Spring дает нам пощечину при запуске:
|
1
2
3
4
5
6
7
8
9
|
UnsatisfiedDependencyException: Error creating bean with name 'caller' defined in file ...UnsatisfiedDependencyException: Error creating bean with name 'fifth' defined in file ...UnsatisfiedDependencyException: Error creating bean with name 'enterpriseyManagerFactoryProxyHelperDispatcher' defined in file ...BeanCurrentlyInCreationException: Error creating bean with name 'caller': Requested bean is currently in creation: Is there an unresolvable circular reference? |
Оставайся со мной, я строю кульминацию здесь. Это явно ошибка, которая, к сожалению, может быть исправлена с помощью инжекции поля (или в этом случае сеттера):
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Componentpublic class Caller { @Autowired private List<StringCallable> callables; public String doWork() { return callables.stream() .map(StringCallable::call) .collect(joining("|")); } } |
Отключив создание бина от внедрения (это невозможно с помощью конструктора), мы теперь можем создать круговой граф зависимостей, в котором Caller содержит экземпляр класса Fifth который ссылается на Enterprisey... , который в свою очередь ссылается на тот же экземпляр Caller . Циклы в графе зависимостей — это запах дизайна, приводящий к не поддерживаемому графику отношений спагетти. Пожалуйста, избегайте их, и если внедрение в конструктор может полностью их предотвратить, это даже лучше.
Встреча getBeansOfType()
Интересно, что есть еще одно решение, которое идет прямо к внутренностям Spring:
ListableBeanFactory.getBeansOfType() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Componentpublic class Caller { private final List<StringCallable> callables; @Autowired public Caller(ListableBeanFactory beanFactory) { callables = new ArrayList<>(beanFactory.getBeansOfType(StringCallable.class).values()); } public String doWork() { return callables.stream() .map(StringCallable::call) .collect(joining("|")); } } |
Проблема решена? Наоборот! getBeansOfType() будет молча пропускать (ну, есть getBeansOfType() TRACE и DEBUG …) при создании и возвращает только те, которые уже существуют. Поэтому Caller был только что создан и контейнер успешно запущен, в то время как он больше не ссылается на Fifth компонент. Вы можете сказать, что я просил об этом, потому что у нас круговая зависимость, поэтому происходят странные вещи. Но это неотъемлемая особенность getBeansOfType() . Чтобы понять, почему использование getBeansOfType() во время запуска контейнера является плохой идеей , рассмотрим следующий сценарий (неважный код опущен):
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
@Componentclass Alpha { static { log.info("Class loaded"); } @Autowired public Alpha(ListableBeanFactory beanFactory) { log.info("Constructor"); log.info("Constructor (beta?): {}", beanFactory.getBeansOfType(Beta.class).keySet()); log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet()); } @PostConstruct public void init() { log.info("@PostConstruct (beta?): {}", beanFactory.getBeansOfType(Beta.class).keySet()); log.info("@PostConstruct (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet()); } } @Componentclass Beta { static { log.info("Class loaded"); } @Autowired public Beta(ListableBeanFactory beanFactory) { log.info("Constructor"); log.info("Constructor (alpha?): {}", beanFactory.getBeansOfType(Alpha.class).keySet()); log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet()); } @PostConstruct public void init() { log.info("@PostConstruct (alpha?): {}", beanFactory.getBeansOfType(Alpha.class).keySet()); log.info("@PostConstruct (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet()); } } @Componentclass Gamma { static { log.info("Class loaded"); } public Gamma() { log.info("Constructor"); } @PostConstruct public void init() { log.info("@PostConstruct"); }} |
Выходные данные журнала показывают, как Spring внутренне загружает и разрешает классы:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
Alpha: | Class loadedAlpha: | ConstructorBeta: | Class loadedBeta: | ConstructorBeta: | Constructor (alpha?): []Gamma: | Class loadedGamma: | ConstructorGamma: | @PostConstructBeta: | Constructor (gamma?): [gamma]Beta: | @PostConstruct (alpha?): []Beta: | @PostConstruct (gamma?): [gamma]Alpha: | Constructor (beta?): [beta]Alpha: | Constructor (gamma?): [gamma]Alpha: | @PostConstruct (beta?): [beta]Alpha: | @PostConstruct (gamma?): [gamma] |
Spring Framework сначала загружает Alpha и пытается создать экземпляр bean-компонента. Однако при запуске getBeansOfType(Beta.class) он обнаруживает Beta поэтому приступает к загрузке и getBeansOfType(Beta.class) его экземпляра. Внутри Beta мы можем сразу определить проблему: когда Beta запрашивает beanFactory.getBeansOfType(Alpha.class) она не дает результатов ( [] ). Spring молча игнорирует Alpha , потому что он в данный момент находится в стадии разработки. Позже все происходит так, как ожидается: Gamma загружается, конструируется и вводится, Beta видит Gamma и когда мы возвращаемся в Alpha , все на месте. Обратите внимание, что даже перемещение getBeansOfType() в метод @PostConstruct не помогает — эти обратные вызовы выполняются не в конце, когда создаются все компоненты, а во время запуска контейнера.
Предложения
getBeansOfType() редко требуется и оказывается непредсказуемым, если у вас есть циклические зависимости. Конечно, вы должны избегать их в первую очередь, и если вы правильно внедрите зависимости через коллекции, Spring может предсказуемо обработать жизненный цикл всех bean-компонентов и либо правильно связать их, либо завершить с ошибкой во время выполнения. При наличии циклических зависимостей между компонентами (иногда случайных или очень длинных с точки зрения узлов и ребер в графе зависимостей) getBeansOfType() может давать разные результаты в зависимости от факторов, которые мы не можем контролировать, например, порядок CLASSPATH.
PS: Слава Якубу Кубринскому за устранение неполадок getBeansOfType() .
| Ссылка: | Spring: добавление списков, карт, опций и ошибок getBeansOfType () от нашего партнера по JCG Томаша Нуркевича в блог Java и соседей . |