Вы здесь:

Spring boot велик и могуч, но есть есть и другие инструменты.

capusta

Camel позволяет абстрагироваться от кода и сосредоточиться на конфигурации, взаимодействии компонент. Camel это интеграционный клей, который соединяет компоненты системы, предоставляя коннекторы. Примеры коннекторов: rest, ftp, Spring bean и еще много всяких: https://camel.apache.org/components/next/index.html#_components.

Небольшой пример.
Задача - разослать обновления цен из офиса в подразделения (торговые точки). И офис и подразделения имеют свои сервера с развернутыми Spring Boot Applications. Ниже примеры xml Spring context и Camel конфигураций из проекта https://github.com/cherepakhin/el59. Проект старый, предприятия давно уже нет, поэтому опубликовал на github. Почему Spring context задан в xml, а не использованы аннотации? Удобнее было менять логику работы комплекса БЕЗ перекомпиляции.) В примере настроено несколько конфигураций Camel (тег route) для рассылки обновлений в торговые точки из центра.
Camel context загружается вместе со Spring context. Подключение конфигурации Camel (camel.xml) в Spring context.
springContext.xml:

<beans>
<?xml version="1.0" encoding="UTF-8"?>
  <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <import resource="dao.xml" /> <!--Spring DAO -->
  <import resource="services.xml" /> <!--Spring services -->

  <import resource="camel.xml"/> <!--CAMEL!!! -->

В dao.xml и services.xml определены Spring beans. Пример определения Spring bean:

<beans>
  <!-- Configure Spring beans -->
  <bean id="senderBestTags" class="ru.perm.v.office.service.SenderBestTags">
	<property name="shopProvider" ref="shopDao"/>
	<property name="bestTagToPriceConvertor" ref="bestTagToPriceConvertor"/>
	<property name="emailer" ref="emailer"/>
  </bean>
...
</beans>

(BestTag - это пользовательский класс, описание товара, его цена и т.п. Значения задаются в центре и должны быть синхронизированы между центром и подразделениями.)

Класс SenderBestTags:

/**
* Отправка изменений ценников в магазин
*/
public class SenderBestTags {
private IEmailer emailer; // какая-то реализация сервиса почты, 
// н.п. https://docs.spring.io/spring-framework/reference/integration/email.html
public Object process(@Body Object body) { emailer.send(emailReceivers, getMailMessage(), subject, attachFiles); } public IEmailer getEmailer() { return emailer; } public void setEmailer(IEmailer emailer) { this.emailer = emailer; } }

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

MessageEntity - контейнер сообщения для Camel (или не Camel, в том смысле, что это простой класс никак несвязанный ни со Spring, ни с Camel ):

public class MessageEntity<T> implements Serializable {
  /*
  * Сообщение (команда) для удаленной системы
  *  Пример:  создать (typeCommand = Create)
  *           строку в накладной (className = DocDetail)
  *           для магазина (shopCod='07443')
  *           с содержимым из entity
  */
  protected TypeCommand typeCommand;
  // тип сущности (н.п. DocDetail)
  protected String className;
  // в какой торговой точке (маршрут)
  protected String shopCod;
  // содержимое сообщения (см. ниже "Пример сообщения из очереди"
  //   &lt;entity type="DocDetail"&gt;...&lt;/entity&gt;  )
  protected T entity;

  public MessageEntity() {
    this.typeCommand = TypeCommand.CREATE;
    this.className = "";
    this.shopCod = "";
  }

  public TypeCommand getTypeCommand() {
    return this.typeCommand;
  }

  public void setTypeCommand(TypeCommand typeCommand) {
    this.typeCommand = typeCommand;
  }

  public T getEntity() {
    return this.entity;
  }

  public void setEntity(T entity) {
    this.entity = entity;
  }
  ...
}

camel.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                      http://camel.apache.org/schema/spring
                      http://camel.apache.org/schema/spring/camel-spring.xsd">


  <import resource="camel_route.xml"/> <!--CAMEL ROUTE!!! См.ниже -->
  <!--Spring beans for Camel-->
  <bean id="ConvertorXmlBestTags" class="ru.perm.v.office.camelcontext.receiver.ConvertorXmlBestTags">
    <property name="type" value="ru.perm.v.office.dto.BestTags" />
    <property name="entity" value="ru.perm.v.office.dto.BestTags" />
    <property name="protocolForTag" ref="protocolForTag"/>
  </bean>
  ...
</beans>

camel_route.xml (ключевые теги для понимания Camel: route, from, when, to):

<?xml version="1.0" encoding="UTF-8"?>
<!-- Configures the Camel route -->
  <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="
              http://www.springframework.org/schema/beans
              http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
              http://camel.apache.org/schema/spring
              http://camel.apache.org/schema/spring/camel-spring.xsd">
    ...
    <camelContext id="camel" xmlns="http://camel.apache.org/schema/spring">

      <propertyPlaceholder id="properties" location="classpath:other.properties"/>
      <!-- route!-->
      <route>
        <from uri="jms:queue:to_shop" />
        <when>
          <xpath>//shopCod = '07260'</xpath>
            <to uri="{{ftp}}/mqto07260?username={{ftp-user}}&password={{ftp-password}}" />
        </when>
        <when>
          <xpath>//shopCod = '07443'</xpath>
            <to uri="{{ftp}}/mqto07443?username={{ftp-user}}&password={{ftp-password}}" />
        </when>
        ...
        <when>
          <xpath>shopCod ='*'</xpath>
            <multicast>
              <to uri="{{ftp}}/mqto07260?username={{ftp-user}}&password={{ftp-password}}&reconnectDelay=60000" />
              <to uri="{{ftp}}/mqto07863?username={{ftp-user}}&password={{ftp-password}}&reconnectDelay=1000" />
              <to uri="{{ftp}}/mq?username={{ftp-user}}&password={{ftp-password}}&reconnectDelay=60000" />
              <to uri="file:c:/temp/?fileName=debug_monitor.xml" />
            </multicast>
        </when>
        <otherwise/>
      </route>
      <route>
        <from uri="jms:queue:updateq" />
        <bean ref="ConvertorFromOfficeDB" method="getXML" />
        <to uri="jms:queue:to_shop" />
      </route>

      <route>
        <from uri="quartz2://myGroup/changenn?cron=0+57+09+*+*+?" />
          <bean ref="creatorChangeNN" method="process" />
          <to
            uri="{{ftp}}/tmp/data/?fileName=CHANGENN.XML&username={{ftp-user}}&password={{ftp-password}}&binary=true" />
      </route>
      ...
    </camelContext>

from uri="jms:queue:to_shop": из очереди "to_shop" ActiveMQ (это сервис работы с очередями сообщений , наподобие Kafka) принимается xml файл-задание на отправку сообщения. Получение сообщения из ActiveMQ служит сигналом для запуска обработки (маршрута). Пример сообщения из очереди:

<?xml version="1.0" encoding="UTF-8"?>
<message>
  <command>CREATE</command>
  <className>DocDetail</className>
  <shopCod>07260</shopCod> <!-- shop cod!!! need for camel route -->
  <entity type="DocDetail">
    <n>23166</n>
    <docn>14941</docn>
    <nnum>71069178</nnum>
    <qty>1.0</qty>
    <price>10000.0</price>
  </entity>
</message>

В зависимости от тега shopCod используя XPath ("<xpath>//shopCod = '07260'</xpath>" в route.xml) файл отправляется по тому или иному маршруту Camel. Конфигурация camel.xml:

<beans  xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                      http://camel.apache.org/schema/spring
                      http://camel.apache.org/schema/spring/camel-spring.xsd">


  <import resource="camel_route.xml"/> 
  
  <bean id="ConvertorXmlBestTags" class="ru.perm.v.office.camelcontext.receiver.ConvertorXmlBestTags">
    <property name="type" value="ru.perm.v.office.dto.BestTags" />
    <property name="entity" value="ru.perm.v.office.dto.BestTags" />
    <property name="protocolForTag" ref="protocolForTag"/>
  
  ...

использованы следующие компоненты Camel:

  • from - источник сообщения
  • jms:queue:updateq - получение сообщения из очереди ActiveMQ ("updateq" - топик)
  • quartz2 - планировщик
  • bean - отправка сообщения в Spring bean (подробнее об этом ниже)
  • multicast - отправка сообщения по нескольким маршрутам

{{ftp}}, {{ftp-user}}, {{ftp-password}} - значения из jdbc.properties в camel_route.xml (см. выше).

ftp=ftp://127.0.0.1
ftp-user=night
ftp-password=n123

События для старта обработки ("from"):

  • 'from quarz' - запуск по таймеру
  • 'from uri="{{ftp}}...' - запуск сканирования ftp папки
  • 'from uri="jms:queue:to_shop' - появилось сообщение в очереди ActiveMQ "to_shop"

Вызов метода process из Spring bean senderBestTags по таймеру (конфигурация. См. начало статьи "Camel позволяет абстрагироваться от кода..."):

<from uri="quartz2://myGroup/changenn?cron=0+57+09+*+*+?" />
    <bean ref="senderBestTags" method="process" />

Java код Spring bean класса SenderBestTags (компонета. См. начало статьи "Camel позволяет абстрагироваться от кода..."):

import org.apache.camel.Body; // body camel-message
import ru.perm.v.office.dto.message.MessageBestTags;
...
public class SenderBestTags {
  ...
  private IEmailer emailer;
  ...
  public Object process(@Body Object body) { // body camel-message
    // MessageBestTags - частный dto класс проекта. См.ниже.
    MessageBestTags messageBestTags = (MessageBestTags) body;
    ...
	emailer.send(getEmail(), getMailMessage(),getMailSubject(), getAttaches());
    ...
  }
}

MessageBestTags:

package ru.perm.v.office.dto.message;
    import ru.perm.v.office.dto.BestTags;

    public class MessageBestTags extends MessageEntity {
      protected BestTags entity;

      public MessageBestTags() {
        this.className = BestTags.class.getSimpleName();
      }

      public BestTags getEntity() {
        return this.entity;
      }

      public void setEntity(BestTags entity) {
        this.entity = entity;
      }
    }
    

Можно описывать маршруты в коде (статья на Habr Apache Camel и Spring Boot)

@Component
class TimedJobs extends RouteBuilder {

@Override
public void configure() {
  from("timer:new-discount?delay=1000&amp;period={{discount.newDiscountPeriod:2000}}")
    .routeId("make-discount")
    .bean("discountService", "makeDiscount")
    .to("jpa:org.apache.camel.example.spring.boot.rest.jpa.Discount")
    .log("Created %${body.amount} discount for ${body.product.name}");

    // additional route...
}

В начале статьи я объяснил почему я использовал xml, а не код. (Удобнее менять логику работы коплекса БЕЗ перекомпиляции.)

Maven зависимости Camel в pom.xml проекта:

<properties>
  <spring.group>org.springframework</spring.group>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <spring.version>3.0.5.RELEASE</spring.version>
  <camel.version>2.13.1</camel.version>
</properties>
...
<dependencies>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-core</artifactId>
      <version>${camel.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-quartz2</artifactId>
      <version>${camel.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-ftp</artifactId>
      <version>${camel.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-jms</artifactId>
      <version>${camel.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-mail</artifactId>
      <version>${camel.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-xstream</artifactId>
      <version>${camel.version}</version>
    </dependency>

...
</dependencies>

Если используется конфигурирование с application.yaml и аннотации, то для запуска подсистемы Camel нужно добавить в application.yaml:

camel:
  springboot:
    main-run-controller: true

Полный пример в https://github.com/cherepakhin/camel_rest/blob/dev/src/main/resources/application.yaml

Говоря о Camel нужно упомянуть о Spring Integration. Простой пример: (TODO: дать ссылку на мебельщиков), Sample-integration, Spring Integration — динамические потоки данных (Habr). Если в "чистом" Camel используются простые Spring beans (аннотация @Bean) и потом они подаются в Camel для использования, то Spring Integration как-бы "подминает" под себя Camel. "Spring Integration (не только, но в основном) посвящена обмену сообщениями . Spring Integration — это, по сути, встроенная корпоративная сервисная шина , которая позволяет вам легко подключать вашу бизнес-логику к каналам обмена сообщениями" (Spring Integration – разработка приложения с нуля (часть 1 / 2)).

Что-то типа отладки.
Есть код SimpleRouteBuilder.java:

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.rest.RestBindingMode;
import org.springframework.stereotype.Component;

@Component
public class SimpleRouteBuilder  extends RouteBuilder {
    @Override
    public void configure() throws Exception {

        // Camel REST service here. Using the Camel servlet component
        restConfiguration()
                .bindingMode(RestBindingMode.auto);

        rest("/say")
                .description("Simple Route REST service")
                .consumes("application/json")
                .produces("application/json")
                .outType(Todo.class)
                .responseMessage().code(200).message("All todos successfully returned").endResponseMessage()
                .to("bean:todoService?method=getSimple");
    }
}

И можно этот код запустить и отдебажить. Для этого нужно перейти в каталог java-файла с Camel компонентой и выполнить:

@v:~/prog/java/camel/camel_try_1/src/main/java/ru/perm/v/camel_try_1$ camel run SimpleRouteBuilder.java --dev
INFO 146219 --- [           main] org.apache.camel.main.MainSupport        : Apache Camel (JBang) 4.8.0 is starting
INFO 146219 --- [           main] org.apache.camel.main.MainSupport        : Using Java 17.0.13 with PID 146219. Started by vasi in /home/vasi/prog/java/camel/camel_try_1/src/main/java/ru/perm/v/camel_try_1
INFO 146219 --- [           main] org.apache.camel.main.ProfileConfigurer  : The application is starting with profile: dev
INFO 146219 --- [           main] he.camel.cli.connector.LocalCliConnector : Camel JBang CLI enabled
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext : Apache Camel 4.8.0 (SimpleRouteBuilder) is starting
INFO 146219 --- [           main] vertx.core.spi.resolver.ResolverProvider : Using the default address resolver as the dns resolver could not be loaded
INFO 146219 --- [ntloop-thread-0] tform.http.vertx.VertxPlatformHttpServer : Vert.x HttpServer started on 0.0.0.0:8080
INFO 146219 --- [           main] upport.FileWatcherResourceReloadStrategy : Live route reloading enabled (directory: .)
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext : Routes startup (total:3 rest-dsl:3)
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext :     Started route1 (rest://get:/say:/hello)
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext :     Started route2 (rest://get:/say:/bye)
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext :     Started route3 (rest://post:/say:/bye)
INFO 146219 --- [           main] e.camel.impl.engine.AbstractCamelContext : Apache Camel 4.8.0 (SimpleRouteBuilder) started in 974ms (build:0ms init:0ms start:974ms)
INFO 146219 --- [           main] ponent.platform.http.main.MainHttpServer : HTTP endpoints summary
INFO 146219 --- [           main] ponent.platform.http.main.MainHttpServer :     http://0.0.0.0:8080/say/bye      (GET)    (accept:application/json)
INFO 146219 --- [           main] ponent.platform.http.main.MainHttpServer :     http://0.0.0.0:8080/say/hello    (GET)

HTTP запрос (httpie):

@v:~$ http  http://0.0.0.0:8080/say/hello
HTTP/1.1 200 OK
Content-Type: application/json
transfer-encoding: chunked

"Hello World"

Ссылки: