Использование подпотоков в Spring Integration

Использование подпотоков в Spring Integration

1. обзор

Spring Integration упрощает использование некоторыхEnterprise Integration Patterns. Один из таких способов - черезits DSL.

В этом руководстве мы рассмотрим поддержку подпотоков в DSL, чтобы упростить некоторые из наших конфигураций.

2. Наша задача

Допустим, у нас есть последовательность целых чисел, которую мы хотим разделить на три разных сегмента.

И если мы хотим использовать Spring Integration для этого, мы могли бы начать с создания трех выходных каналов:

  • Цифры вроде 0, 3, 6 и 9 перейдут кmultipleOfThreeChannel

  • Цифры вроде 1, 4, 7 и 10 перейдут кremainderIsOneChannel

  • А числа вроде 2, 5, 8 и 11 идут вremainderIsTwoChannel

Чтобы увидеть, насколько полезными могут быть подпотоки, давайте начнем с того, как это будет выглядеть без подпотоков.

А затем мы будем использовать подпотоки, чтобы упростить нашу конфигурацию:

  • publishSubscribeChannel

  • routeToRecipients

  • Filters, чтобы настроить нашу логикуif-then

  • Routers, чтобы настроить нашу логикуswitch

3. Предпосылки

Теперь, прежде чем настраивать наши подпотоки, давайте создадим эти выходные каналы.

Мы сделаем этиQueueChannels, так как это немного проще для демонстрации:

@EnableIntegration
@IntegrationComponentScan
public class SubflowsConfiguration {

    @Bean
    QueueChannel multipleOfThreeChannel() {
        return new QueueChannel();
    }

    @Bean
    QueueChannel remainderIsOneChannel() {
        return new QueueChannel();
    }

    @Bean
    QueueChannel remainderIsTwoChannel() {
        return new QueueChannel();
    }

    boolean isMultipleOfThree(Integer number) {
       return number % 3 == 0;
    }

    boolean isRemainderIOne(Integer number) {
        return number % 3 == 1;
    }

    boolean isRemainderTwo(Integer number) {
        return number % 3 == 2;
    }
}

В конечном счете, это то, где наши сгруппированные числа в конечном итоге.

Также обратите внимание, что Spring Integration может легко начать выглядеть сложным, поэтому мы добавим несколько вспомогательных методов для удобства чтения.

4. Решение без подпотоков

Теперь нам нужно определить наши потоки.

Без подпотоков простая идея состоит в том, чтобы определить три отдельных потока интеграции, по одному для каждого типа числа.

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

4.1. Определение компонентовIntegrationFlow

Во-первых, давайте определим каждый bean-компонентIntegrationFlow в нашем классеSubflowConfiguration :

@Bean
public IntegrationFlow multipleOfThreeFlow() {
    return flow -> flow.split()
      . filter(this::isMultipleOfThree)
      .channel("multipleOfThreeChannel");
}

Наш поток содержит две конечные точки -Splitter , за которым следуетFilter.

Фильтр делает то, на что это похоже. Но зачем нам сплиттер? Мы увидим это через минуту, но в основном он разбивает вводCollection на отдельные сообщения.

И мы, конечно, можем таким же образом определить еще два bean-компонентаIntegrationFlow.

4.2. Шлюзы обмена сообщениями

Для каждого потока нам также понадобитсяMessage Gateway.

Проще говоря, они абстрагируют API-интерфейс сообщений Spring Integration от вызывающей стороны, подобно тому, как служба REST может абстрагировать HTTP:

@MessagingGateway
public interface NumbersClassifier {

    @Gateway(requestChannel = "multipleOfThreeFlow.input")
    void multipleOfThree(Collection numbers);

    @Gateway(requestChannel = "remainderIsOneFlow.input")
    void remainderIsOne(Collection numbers);

    @Gateway(requestChannel = "remainderIsTwoFlow.input")
    void remainderIsTwo(Collection numbers);

}

Для каждого нам нужно использовать саннотацию@Gateway и указать неявное имя для входного канала, которое является просто именем bean-компонента, за которым следует“.input”. Note that we can use this convention because we are using lambda-based flows.

Эти методы являются точками входа в наши потоки.

4.3. Отправка сообщений и проверка вывода

А теперь давайте протестируем:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { SeparateFlowsConfiguration.class })
public class SeparateFlowsUnitTest {

    @Autowired
    private QueueChannel multipleOfThreeChannel;

    @Autowired
    private NumbersClassifier numbersClassifier;
    @Test
    public void whenSendMessagesToMultipleOf3Flow_thenOutputMultiplesOf3() {
        numbersClassifier.multipleOfThree(Arrays.asList(1, 2, 3, 4, 5, 6));
        Message outMessage = multipleOfThreeChannel.receive(0);
        assertEquals(outMessage.getPayload(), 3);
        outMessage = multipleOfThreeChannel.receive(0);
        assertEquals(outMessage.getPayload(), 6);
        outMessage = multipleOfThreeChannel.receive(0);
        assertNull(outMessage);
    }
}

Обратите внимание, что мы отправили сообщения какList, поэтому нам нужен разделитель, чтобы взять одно «сообщение списка» и преобразовать его в несколько «числовых сообщений».

Мы вызываемreceive с помощьюo, чтобы получить следующее доступное сообщение без ожидания. Поскольку в нашем списке два числа, кратные трем, мы ожидаем, что сможем вызвать его дважды. Третий вызовreceive  возвращаетnull.

receive, oft, возвращаетMessage, поэтому мы вызываемgetPayload для извлечения числа.

Точно так же мы могли бы сделать то же самое для двух других.

So, that was the solution without subflows. У нас есть три отдельных потока для обслуживания и три отдельных метода шлюза.

Что мы сейчас сделаем, так это заменим триIntegrationFlow bean-компонента одним компонентом, а три метода шлюза - одним.

5. ИспользуяpublishSubscribeChannel

МетодpublishSubscribeChannel() рассылает сообщения всем подпотокам подписки. Таким образом, мы можем создать один поток вместо трех.

@Bean
public IntegrationFlow classify() {
    return flow -> flow.split()
        .publishSubscribeChannel(subscription ->
           subscription
             .subscribe(subflow -> subflow
               . filter(this::isMultipleOfThree)
               .channel("multipleOfThreeChannel"))
             .subscribe(subflow -> subflow
                . filter(this::isRemainderOne)
                .channel("remainderIsOneChannel"))
             .subscribe(subflow -> subflow
                . filter(this::isRemainderTwo)
                .channel("remainderIsTwoChannel")));
}

Таким образом,the subflows are anonymous, meaning that they can’t be independently addressed.

Теперь у нас есть только один поток, поэтому давайте также отредактируем нашNumbersClassifier :

@Gateway(requestChannel = "classify.input")
void classify(Collection numbers);

Теперьsince we have only one IntegrationFlow bean and one gateway method, we need only send our list once:

@Test
public void whenSendMessagesToFlow_thenNumbersAreClassified() {
    numbersClassifier.classify(Arrays.asList(1, 2, 3, 4, 5, 6));

    // same assertions as before
}

Обратите внимание, что с этого момента изменится только определение потока интеграции, поэтому мы больше не будем показывать тест.

6. ИспользуяrouteToRecipients

Другой способ добиться того же -routeToRecipients, что приятно, потому что в него встроена фильтрация.

Используя этот метод,we can specify both channels and subflows for broadcasting. 

6.1. recipientс

В приведенном ниже коде мы укажемmultipleof3Channel,remainderIs1Channel, иremainderIsTwoChannel в качестве получателей в соответствии с нашими условиями:

@Bean
public IntegrationFlow classify() {
    return flow -> flow.split()
        .routeToRecipients(route -> route
          . recipient("multipleOfThreeChannel",
            this::isMultipleOfThree)
          . recipient("remainderIsOneChannel",
            this::isRemainderOne)
          . recipient("remainderIsTwoChannel",
            this::isRemainderTwo));
}

Мы также можем вызватьrecipient без условия, иrouteToRecipients будет безоговорочно публиковать в этом месте назначения.

6.2. recipientFlowс

И обратите внимание, чтоrouteToRecipients позволяет нам определять полный поток, какpublishSubscribeChannel. 

Давайте изменим приведенный выше код и укажемanonymous subflow as the first recipient:

.routeToRecipients(route -> route
  .recipientFlow(subflow -> subflow
      . filter(this::isMultipleOfThree)
      .channel("mutipleOfThreeChannel"))
  ...);

This subflow will receive the entire sequence of messages,, поэтому нам нужно отфильтровать, как раньше, чтобы получить такое же поведение.

И снова нам хватило одного bean-компонентаIntegrationFlow.

Теперь перейдем к компонентамif-else. Один из них -Filter.

7. Использованиеif-then потоков

Мы уже использовалиFilter во всех предыдущих примерах. Хорошая новость в том, что мы можем указать не только условие для дальнейшей обработки, но иa channel or aflow for the discarded messages.

Мы можем думать об отбрасывании потоков и каналов как о sblockelse :

@Bean
public IntegrationFlow classify() {
    return flow -> flow.split()
        . filter(this::isMultipleOfThree,
           notMultiple -> notMultiple
             .discardFlow(oneflow -> oneflow
               . filter(this::isRemainderOne,
                 twoflow -> twoflow
                   .discardChannel("remainderIsTwoChannel"))
               .channel("remainderIsOneChannel"))
        .channel("multipleofThreeChannel");
}

В этом случае мы реализовали нашу логику маршрутизацииif-else:

  • If число не кратно трем,then отклоняет эти сообщения в поток отклонения; we use a flow here since there is more logic needed to know its destination channel.

  • В потоке сбросаif число не является остатком единицы,then отбрасывает эти сообщения в канал сброса.

8. switch-по рассчитанному значению

И, наконец, давайте попробуем методroute , который дает нам немного больше контроля, чемrouteToRecipients. . Это хорошо, потому чтоRouter может разбить поток на любое количество частей, тогда как сканированиеFilter только два.

8.1. channelMappingс

Давайте определим наш bean-компонентIntegrationFlow:

@Bean
public IntegrationFlow classify() {
    return classify -> classify.split()
      . route(number -> number % 3,
        mapping -> mapping
         .channelMapping(0, "multipleOfThreeChannel")
         .channelMapping(1, "remainderIsOneChannel")
         .channelMapping(2, "remainderIsTwoChannel"));
}

В приведенном выше коде мы вычисляем ключ маршрутизации, выполняя деление:

route(p -> p % 3,...

На основе этого ключа мы маршрутизируем сообщения:

channelMapping(0, "multipleof3Channel")

8.2. subFlowMappingс

Теперь, как и в случае с другими, мы можем получить больший контроль, указав подпоток, заменивchannelMapping наsubFlowMapping:

.subFlowMapping(1, subflow -> subflow.channel("remainderIsOneChannel"))

Или еще больше контроля, вызвав методhandle вместо методаchannel :

.subFlowMapping(2, subflow -> subflow
  . handle((payload, headers) -> {
      // do extra work on the payload
     return payload;
  }))).channel("remainderIsTwoChannel");

В этом случае подпоток вернется к основному потоку после методаroute(), поэтому нам нужно будет указать каналremainderIsTwoChannel.

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

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

Как обычно, доступен полный исходный кодon GitHub.