Если вы используете 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> { } @Component class Third implements StringCallable { @Override public String call() { return "3" ; } } @Component class Forth implements StringCallable { @Override public String call() { return "4" ; } } @Component class 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 { } @Component class Third implements StringCallable { //... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } } @Component class Forth implements StringCallable { //... @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1 ; } } @Component class 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
|
@Component public 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 @Primary public 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
|
@Component public 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
|
@Component class 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
|
@Component public 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
|
@Component public 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
|
@Component class 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()); } } @Component class 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()); } } @Component class 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 loaded Alpha: | Constructor Beta: | Class loaded Beta: | Constructor Beta: | Constructor (alpha?): [] Gamma: | Class loaded Gamma: | Constructor Gamma: | @PostConstruct Beta: | 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 и соседей . |