Весна — это основа, которая не перестает меня удивлять. Это связано с тем, что он предлагает множество различных решений, которые позволяют нам, разработчикам, выполнять наши задачи без написания миллионов строк кода. Вместо этого мы можем сделать то же самое в гораздо более удобочитаемой, стандартизированной форме. В этом посте я постараюсь описать одну из его особенностей, которая, скорее всего, всем вам известна, но, на мой взгляд, ее важность недооценена. Особенностью, о которой я буду говорить, является аннотация @Primary .
Эта проблема
В нескольких проектах, над которыми я работал, мы столкнулись с общей бизнес-проблемой — у нас была точка входа в более сложную логику — некоторый контейнер, который собирал бы результаты нескольких других процессоров в один вывод (что-то вроде функции map-filter-Reduce из функционального программирования). В какой-то степени это напоминало шаблон Composite . Собрав все вместе, наш подход был следующим:
- У нас был контейнер, в котором был список процессоров с автопроводкой, реализующих общий интерфейс
- Наш контейнер реализовал тот же интерфейс, что и элементы списка автопроводки
- Мы хотели, чтобы клиентский класс, который использовал бы контейнер, имел всю прозрачность всей обработки — он заинтересован только в результате
- Процессоры имеют некоторую логику (предикат), на основе которой процессор применим к текущему набору входных данных.
- Результаты обработки были затем объединены в список, а затем сведены к одному выводу.
Существует множество способов решения этой проблемы — я представлю тот, который использует Spring с аннотацией @Primary.
Решение
Давайте начнем с определения того, как наш вариант использования будет соответствовать вышеупомянутым предварительным условиям. Наш набор данных — это класс Person, который выглядит следующим образом:
Person.java
|
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
|
package com.blogspot.toomuchcoding.person.domain;public final class Person { private final String name; private final int age; private final boolean stupid; public Person(String name, int age, boolean stupid) { this.name = name; this.age = age; this.stupid = stupid; } public String getName() { return name; } public int getAge() { return age; } public boolean isStupid() { return stupid; }} |
Ничего необычного. Теперь давайте определим контракт:
PersonProcessingService.java
|
1
2
3
4
5
6
7
8
|
package com.blogspot.toomuchcoding.person.service;import com.blogspot.toomuchcoding.person.domain.Person;public interface PersonProcessingService { boolean isApplicableFor(Person person); String process(Person person);} |
Как указано в предварительных условиях, каждая реализация PersonProcessingService должна определить два пункта контракта:
- применимо ли это для текущего лица
- как это обрабатывает человека.
Теперь давайте рассмотрим некоторые из имеющихся у нас процессоров — я не буду публиковать здесь код, потому что он бессмысленен — вы можете проверить код позже на Github или в Bitbucket . У нас есть следующие аннотированные @Component реализации PersonProcessingService:
- AgePersonProcessingService
- применимо, если возраст лица больше или равен 18
- возвращает строку, содержащую «AGE», когда происходит обработка — это глупо, но это просто демо, верно? 🙂
- IntelligencePersonProcessingService
- применимо, если человек глуп
- возвращает строку, содержащую «STUPID» в процессе обработки
- NamePersonProcessingService
- применимо, если у Лица есть имя
- возвращает строку, содержащую «NAME» в процессе обработки
Логика довольно проста. Теперь наш контейнер PersonProcessingServices захочет выполнить итерацию для данного Person над процессорами, проверить, применим ли текущий процессор (фильтр), и в этом случае добавить строку, являющуюся результатом обработки Person, в список ответов. (map — функция, преобразующая Person в строку) и, наконец, соедините эти ответы запятой (уменьшите). Давайте посмотрим, как это делается:
PersonProcessingServiceContainer.java
|
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
|
package com.blogspot.toomuchcoding.person.service;import java.util.ArrayList;import java.util.List;import org.apache.commons.lang.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Primary;import org.springframework.stereotype.Component;import com.blogspot.toomuchcoding.person.domain.Person;@Component@Primaryclass PersonProcessingServiceContainer implements PersonProcessingService { private static final Logger LOGGER = LoggerFactory.getLogger(PersonProcessingServiceContainer.class); @Autowired private List<PersonProcessingService> personProcessingServices = new ArrayList<PersonProcessingService>(); @Override public boolean isApplicableFor(Person person) { return person != null; } @Override public String process(Person person) { List<String> output = new ArrayList<String>(); for(PersonProcessingService personProcessingService : personProcessingServices){ if(personProcessingService.isApplicableFor(person)){ output.add(personProcessingService.process(person)); } } String result = StringUtils.join(output, ","); LOGGER.info(result); return result; } public List<PersonProcessingService> getPersonProcessingServices() { return personProcessingServices; }} |
Как вы можете видеть, у нас есть контейнер, аннотированный @Primary, что означает, что если необходимо внедрить реализацию PersonProcessingService, тогда Spring выберет PersonProcessingServiceContainer для внедрения. Круто то, что у нас есть список автоматически подключенных PersonProcessingServices, что означает, что все другие реализации этого интерфейса будут автоматически подключаться (контейнер не будет автоматически подключаться к списку!).
Теперь давайте проверим тесты Спока, которые доказывают, что я не лгу. Если вы еще не используете Spock в своем проекте, вам следует сразу же переместить его.
PersonProcessingServiceContainerIntegrationSpec.groovy
|
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
55
56
57
58
59
60
61
62
63
64
65
66
|
package com.blogspot.toomuchcoding.person.serviceimport com.blogspot.toomuchcoding.configuration.SpringConfigurationimport com.blogspot.toomuchcoding.person.domain.Personimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.test.context.ContextConfigurationimport spock.lang.Specificationimport spock.lang.Unrollimport static org.hamcrest.CoreMatchers.notNullValue@ContextConfiguration(classes = [SpringConfiguration])class PersonProcessingServiceContainerIntegrationSpec extends Specification { @Autowired PersonProcessingService personProcessingService def "should autowire container even though there are many implementations of service"(){ expect: personProcessingService instanceof PersonProcessingServiceContainer } def "the autowired container should not have itself in the list of autowired services"(){ expect: personProcessingService instanceof PersonProcessingServiceContainer and: !(personProcessingService as PersonProcessingServiceContainer).personProcessingServices.findResult { it instanceof PersonProcessingServiceContainer } } def "should not be applicable for processing if a person doesn't exist"(){ given: Person person = null expect: !personProcessingService.isApplicableFor(person) } def "should return an empty result for a person not applicable for anything"(){ given: Person person = new Person("", 17, false) when: def result = personProcessingService.process(person) then: result notNullValue() result.isEmpty() } @Unroll("For name [#name], age [#age] and being stupid [#stupid] the result should contain keywords #keywords") def "should perform different processing depending on input"(){ given: Person person = new Person(name, age, stupid) when: def result = personProcessingService.process(person) then: keywords.every { result.contains(it) } where: name | age | stupid || keywords "jan" | 20 | true || ['NAME', 'AGE', 'STUPID'] "" | 20 | true || ['AGE', 'STUPID'] "" | 20 | false || ['AGE'] null | 17 | true || ['STUPID'] "jan" | 17 | true || ['NAME'] }} |
Тесты довольно просты:
- Мы доказываем, что поле autowired фактически является нашим контейнером — PersonProcessingServiceContainer.
- Затем мы показываем, что не можем найти объект в коллекции автопроводных реализаций PersonProcessingService, который имеет тип PersonProcessingServiceContainer
- В следующих двух тестах мы докажем, что логика наших процессоров работает
- И последнее, но не менее важное — это лучшее, что предлагает Спок — предложение where, которое позволяет нам создавать прекрасные парамтеризованные тесты.
По модулю
Представьте себе ситуацию, в которой у вас есть реализация интерфейса, определенного в вашем основном модуле.
|
1
2
3
4
|
@Componentclass CoreModuleClass implements SomeInterface {...} |
Что если вы решите в своем другом модуле, который имеет зависимость от основного модуля, что вы не хотите использовать этот CoreModuleClass и хотите иметь некоторую настраиваемую логику везде, где SomeInterface подключен автоматически? Хорошо — используйте @Primary!
|
1
2
3
4
5
|
@Component@Primaryclass CountryModuleClass implements SomeInterface {...} |
Таким образом, вы уверены, что везде, где SomeInterface должен быть автоматически подключен, в поле будет вставлен ваш CountryModuleClass.
Вывод
В этом посте вы можете увидеть, как
- используйте аннотацию @Primary для создания составного контейнера контейнерных реализаций
- используйте аннотацию @Primary, чтобы обеспечить реализацию интерфейса для каждого модуля, которая будет иметь приоритет над другими компонентами @ с точки зрения автоматического подключения
- пиши классные тесты Спока 🙂
Код
Вы можете найти код, представленный здесь, в хранилище Gitub Too Much Coding или в хранилище Bitbucket Too Much Coding .