Статьи

Первичная аннотация Spring в действии

Весна — это основа, которая не перестает меня удивлять. Это связано с тем, что он предлагает множество различных решений, которые позволяют нам, разработчикам, выполнять наши задачи без написания миллионов строк кода. Вместо этого мы можем сделать то же самое в гораздо более удобочитаемой, стандартизированной форме. В этом посте я постараюсь описать одну из его особенностей, которая, скорее всего, всем вам известна, но, на мой взгляд, ее важность недооценена. Особенностью, о которой я буду говорить, является аннотация @Primary .

Эта проблема

В нескольких проектах, над которыми я работал, мы столкнулись с общей бизнес-проблемой — у нас была точка входа в более сложную логику — некоторый контейнер, который собирал бы результаты нескольких других процессоров в один вывод (что-то вроде функции map-filter-Reduce из функционального программирования). В какой-то степени это напоминало шаблон Composite . Собрав все вместе, наш подход был следующим:

  1. У нас был контейнер, в котором был список процессоров с автопроводкой, реализующих общий интерфейс
  2. Наш контейнер реализовал тот же интерфейс, что и элементы списка автопроводки
  3. Мы хотели, чтобы клиентский класс, который использовал бы контейнер, имел всю прозрачность всей обработки — он заинтересован только в результате
  4. Процессоры имеют некоторую логику (предикат), на основе которой процессор применим к текущему набору входных данных.
  5. Результаты обработки были затем объединены в список, а затем сведены к одному выводу.

Существует множество способов решения этой проблемы — я представлю тот, который использует 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 должна определить два пункта контракта:

  1. применимо ли это для текущего лица
  2. как это обрабатывает человека.

Теперь давайте рассмотрим некоторые из имеющихся у нас процессоров — я не буду публиковать здесь код, потому что он бессмысленен — ​​вы можете проверить код позже на 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
@Primary
class 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.service
import com.blogspot.toomuchcoding.configuration.SpringConfiguration
import com.blogspot.toomuchcoding.person.domain.Person
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import spock.lang.Unroll
 
import 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']
    }
}

Тесты довольно просты:

  1. Мы доказываем, что поле autowired фактически является нашим контейнером — PersonProcessingServiceContainer.
  2. Затем мы показываем, что не можем найти объект в коллекции автопроводных реализаций PersonProcessingService, который имеет тип PersonProcessingServiceContainer
  3. В следующих двух тестах мы докажем, что логика наших процессоров работает
  4. И последнее, но не менее важное — это лучшее, что предлагает Спок — предложение where, которое позволяет нам создавать прекрасные парамтеризованные тесты.

По модулю

Представьте себе ситуацию, в которой у вас есть реализация интерфейса, определенного в вашем основном модуле.

1
2
3
4
@Component
class CoreModuleClass implements SomeInterface {
...
}

Что если вы решите в своем другом модуле, который имеет зависимость от основного модуля, что вы не хотите использовать этот CoreModuleClass и хотите иметь некоторую настраиваемую логику везде, где SomeInterface подключен автоматически? Хорошо — используйте @Primary!

1
2
3
4
5
@Component
@Primary
class CountryModuleClass implements SomeInterface {
...
}

Таким образом, вы уверены, что везде, где SomeInterface должен быть автоматически подключен, в поле будет вставлен ваш CountryModuleClass.

Вывод

В этом посте вы можете увидеть, как

  • используйте аннотацию @Primary для создания составного контейнера контейнерных реализаций
  • используйте аннотацию @Primary, чтобы обеспечить реализацию интерфейса для каждого модуля, которая будет иметь приоритет над другими компонентами @ с точки зрения автоматического подключения
  • пиши классные тесты Спока 🙂

Код

Вы можете найти код, представленный здесь, в хранилище Gitub Too Much Coding или в хранилище Bitbucket Too Much Coding .

Ссылка: аннотация SpringPrimary в действии от нашего партнера по JCG Марцина Грэйсчака в блоге для блогеров по кодированию наркоманов .