Введение в Spring Cloud Stream

Введение в Spring Cloud Stream

1. обзор

Spring Cloud Stream - это фреймворк, построенный на основе Spring Boot и Spring Integration, которыйhelps in creating event-driven or message-driven microservices.

В этой статье мы познакомим вас с концепциями и конструкциями Spring Cloud Stream на нескольких простых примерах.

2. Maven Зависимости

Для начала нам нужно добавить зависимостьSpring Cloud Starter Stream with the broker RabbitMQ Maven в качестве промежуточного программного обеспечения для обмена сообщениями в нашpom.xml:


    org.springframework.cloud
    spring-cloud-starter-stream-rabbit
    1.3.0.RELEASE

И мы добавимmodule dependency from Maven Central, чтобы также включить поддержку JUnit:


    org.springframework.cloud
    spring-cloud-stream-test-support
    1.3.0.RELEASE
    test

3. Основные понятия

Архитектура микросервисов работает по принципу «https://martinfowler.com/articles/microservices.html#SmartEndpointsAndDumbPipes[smart конечные точки и тупые каналы]». Связь между конечными точками осуществляется сторонними поставщиками программного обеспечения, такими как RabbitMQ или Apache Kafka. Services communicate by publishing domain events via these endpoints or channels.

Давайте рассмотрим концепции, составляющие структуру Spring Cloud Stream, а также основные парадигмы, о которых мы должны знать, чтобы создавать сервисы, управляемые сообщениями.

3.1. Формирует

Давайте посмотрим на простой сервис в Spring Cloud Stream, который прослушивает привязкуinput и отправляет ответ на привязкуoutput:

@SpringBootApplication
@EnableBinding(Processor.class)
public class MyLoggerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyLoggerServiceApplication.class, args);
    }

    @StreamListener(Processor.INPUT)
    @SendTo(Processor.OUTPUT)
    public LogMessage enrichLogMessage(LogMessage log) {
        return new LogMessage(String.format("[1]: %s", log.getMessage()));
    }
}

Аннотация@EnableBinding настраивает приложение для привязки каналовINPUT иOUTPUT, определенных в интерфейсеProcessor. Both channels are bindings that can be configured to use a concrete messaging-middleware or binder.с

Давайте посмотрим на определение всех этих понятий:

  • Bindings - набор интерфейсов, декларативно идентифицирующих входные и выходные каналы

  • Binder - реализация промежуточного программного обеспечения для обмена сообщениями, такого как Kafka или RabbitMQ

  • Channel - представляет собой канал связи между промежуточным программным обеспечением обмена сообщениями и приложением

  • StreamListeners - методы обработки сообщений в bean-компонентах, которые будут автоматически вызываться для сообщения из канала после того, какMessageConverter выполнит сериализацию / десериализацию между событиями, связанными с промежуточным программным обеспечением, и типами объектов домена / POJO

  • MessageSchemas - используются для сериализации и десериализации сообщений, эти схемы могут быть статически считаны из местоположения или загружены динамически, поддерживая эволюцию типов объектов домена

3.2. Шаблоны общения

Messages designated to destinations are delivered by the Publish-Subscribe messaging pattern. Издатели распределяют сообщения по темам, каждая из которых идентифицируется по имени. Подписчики проявляют интерес к одной или нескольким темам. Промежуточное программное обеспечение фильтрует сообщения, доставляя подписчикам интересные темы.

Теперь подписчики могут быть сгруппированы. consumer group - это набор подписчиков или потребителей, обозначенныхgroup id, в пределах которых сообщения из темы или раздела темы доставляются с балансировкой нагрузки.

4. Модель программирования

В этом разделе описываются основы создания приложений Spring Cloud Stream.

4.1. Функциональное тестирование

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

Давайте отправим сообщение в указанный выше сервисenrichLogMessage и проверим, содержит ли ответ текст“[1]: “ в начале сообщения:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyLoggerServiceApplication.class)
@DirtiesContext
public class MyLoggerApplicationTests {

    @Autowired
    private Processor pipe;

    @Autowired
    private MessageCollector messageCollector;

    @Test
    public void whenSendMessage_thenResponseShouldUpdateText() {
        pipe.input()
          .send(MessageBuilder.withPayload(new LogMessage("This is my message"))
          .build());

        Object payload = messageCollector.forChannel(pipe.output())
          .poll()
          .getPayload();

        assertEquals("[1]: This is my message", payload.toString());
    }
}

4.2. Клиентские каналы

В приведенном выше примере мы использовали интерфейсProcessor, предоставляемый Spring Cloud, который имеет только один входной и один выходной канал.

Если нам нужно что-то другое, например, один входной и два выходных канала, мы можем создать собственный процессор:

public interface MyProcessor {
    String INPUT = "myInput";

    @Input
    SubscribableChannel myInput();

    @Output("myOutput")
    MessageChannel anOutput();

    @Output
    MessageChannel anotherOutput();
}

Spring обеспечит правильную реализацию этого интерфейса для нас. Имена каналов можно задать с помощью аннотаций, например@Output(“myOutput”).

В противном случае Spring будет использовать имена методов в качестве имен каналов. Таким образом, у нас есть три канала:myInput,myOutput иanotherOutput.

Теперь представим, что мы хотим направить сообщения на один выход, если значение меньше 10, и на другой выход, если значение больше или равно 10:

@Autowired
private MyProcessor processor;

@StreamListener(MyProcessor.INPUT)
public void routeValues(Integer val) {
    if (val < 10) {
        processor.anOutput().send(message(val));
    } else {
        processor.anotherOutput().send(message(val));
    }
}

private static final  Message message(T val) {
    return MessageBuilder.withPayload(val).build();
}

4.3. Условная отправка

Используя аннотацию@StreamListener, мы также можемfilter the messages we expect in the consumer, используя любое условие, которое мы определяем с помощьюSpEL expressions.

В качестве примера мы могли бы использовать условную диспетчеризацию в качестве другого подхода для маршрутизации сообщений на разные выходные данные:

@Autowired
private MyProcessor processor;

@StreamListener(
  target = MyProcessor.INPUT,
  condition = "payload < 10")
public void routeValuesToAnOutput(Integer val) {
    processor.anOutput().send(message(val));
}

@StreamListener(
  target = MyProcessor.INPUT,
  condition = "payload >= 10")
public void routeValuesToAnotherOutput(Integer val) {
    processor.anotherOutput().send(message(val));
}

Единственныйlimitation of this approach is that these methods must not return a value.

5. Настроить

Давайте настроим приложение, которое будет обрабатывать сообщение от брокера RabbitMQ.

5.1. Конфигурация связующего

Мы можем настроить наше приложение на использование реализации связующего по умолчанию черезMETA-INF/spring.binders:

rabbit:\
org.springframework.cloud.stream.binder.rabbit.config.RabbitMessageChannelBinderConfiguration

Или мы можем добавить библиотеку связывателей для RabbitMQ в путь к классам, включивthis dependency:


    org.springframework.cloud
    spring-cloud-stream-binder-rabbit
    1.3.0.RELEASE

Если реализация связывателя не предоставлена, Spring будет использовать прямую передачу сообщений между каналами.

5.2. Конфигурация RabbitMQ

Чтобы настроить пример из раздела 3.1 для использования связывателя RabbitMQ, нам нужно обновитьapplication.yml, расположенный вsrc/main/resources:

spring:
  cloud:
    stream:
      bindings:
        input:
          destination: queue.log.messages
          binder: local_rabbit
        output:
          destination: queue.pretty.log.messages
          binder: local_rabbit
      binders:
        local_rabbit:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: 
                port: 5672
                username: 
                password: 
                virtual-host: /

Привязкаinput будет использовать обмен под названиемqueue.log.messages, а привязкаoutput будет использовать обменqueue.pretty.log.messages. Обе привязки будут использовать связкуlocal_rabbit.

Обратите внимание, что нам не нужно заранее создавать обмены RabbitMQ или очереди. При запуске приложенияboth exchanges are automatically created.

Чтобы протестировать приложение, мы можем использовать сайт управления RabbitMQ для публикации сообщения. В панелиPublish Message обменаqueue.log.messages нам нужно ввести запрос в формате JSON.

5.3. Настройка преобразования сообщений

Spring Cloud Stream позволяет нам применять преобразование сообщений для определенных типов контента. В приведенном выше примере вместо использования формата JSON мы хотим предоставить простой текст.

Для этого нужноapply a custom transformation to LogMessage using a MessageConverter:

@SpringBootApplication
@EnableBinding(Processor.class)
public class MyLoggerServiceApplication {
    //...

    @Bean
    public MessageConverter providesTextPlainMessageConverter() {
        return new TextPlainMessageConverter();
    }

    //...
}
public class TextPlainMessageConverter extends AbstractMessageConverter {

    public TextPlainMessageConverter() {
        super(new MimeType("text", "plain"));
    }

    @Override
    protected boolean supports(Class clazz) {
        return (LogMessage.class == clazz);
    }

    @Override
    protected Object convertFromInternal(Message message,
        Class targetClass, Object conversionHint) {
        Object payload = message.getPayload();
        String text = payload instanceof String
          ? (String) payload
          : new String((byte[]) payload);
        return new LogMessage(text);
    }
}

После применения этих изменений, возвращаясь к панелиPublish Message, если мы установим заголовок «contentTypes» на «text/plain» и полезную нагрузку на «Hello World», он должен работать как раньше.

5.4. Группы потребителей

При запуске нескольких экземпляров нашего приложенияevery time there is a new message in an input channel, all subscribers will be notified.

В большинстве случаев нам нужно, чтобы сообщение было обработано только один раз. Spring Cloud Stream реализует это поведение через группы потребителей.

Чтобы включить такое поведение, каждая привязка потребителя может использовать свойствоspring.cloud.stream.bindings.<CHANNEL>.group для указания имени группы:

spring:
  cloud:
    stream:
      bindings:
        input:
          destination: queue.log.messages
          binder: local_rabbit
          group: logMessageConsumers
          ...

6. Микросервисы, управляемые сообщениями

В этом разделе мы представляем все необходимые функции для запуска наших приложений Spring Cloud Stream в контексте микросервисов.

6.1. Увеличение масштаба

Когда работает несколько приложений, важно обеспечить правильное распределение данных между потребителями. Для этого Spring Cloud Stream предоставляет два свойства:

  • spring.cloud.stream.instanceCount - количество запущенных приложений

  • spring.cloud.stream.instanceIndex - индекс текущего приложения

Например, если мы развернули два экземпляра указанного выше приложенияMyLoggerServiceApplication, свойствоspring.cloud.stream.instanceCount должно быть 2 для обоих приложений, а свойствоspring.cloud.stream.instanceIndex должно быть 0 и 1 соответственно.

Эти свойства устанавливаются автоматически, если мы развертываем приложения Spring Cloud Stream с помощью Spring Data Flow, как описано вthis article.

6.2. Разметка

Событиями домена могут быть сообщенияPartitioned. Это помогает, когда мыscaling up the storage and improving application performance.

Событие домена обычно имеет ключ раздела, поэтому он попадает в тот же раздел со связанными сообщениями.

Предположим, мы хотим, чтобы сообщения журнала были разделены по первой букве сообщения, которая будет ключом раздела, и сгруппированы в два раздела.

Будет один раздел для сообщений журнала, которые начинаются сA-M, и другой раздел дляN-Z.. Это можно настроить с помощью двух свойств:

  • spring.cloud.stream.bindings.output.producer.partitionKeyExpression - выражение для разделения полезной нагрузки

  • spring.cloud.stream.bindings.output.producer.partitionCount - количество групп

Sometimes the expression to partition is too complex to write it in only one line. Для этих случаев мы можем написать нашу собственную стратегию разделения, используя свойствоspring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass.

6.3. Индикатор здоровья

В контексте микросервисов нам также потребуетсяdetect when a service is down or starts failing. Spring Cloud Stream предоставляет свойствоmanagement.health.binders.enabled для включения индикаторов работоспособности для связывателей.

При запуске приложения мы можем запросить состояние работоспособности вhttp://<host>:<port>/health.

7. Заключение

В этом руководстве мы представили основные концепции Spring Cloud Stream и показали, как его использовать, на нескольких простых примерах в RabbitMQ. Более подробную информацию о Spring Cloud Stream можно найти наhere.

Исходный код этой статьи можно найти вover on GitHub.