Статьи

Spring: внедрение списков, карт, опций и подводных камней getBeansOfType ()

Если вы используете 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() .