В этой статье рассмотрена интеграция Apache Camel со средой Spring Boot.

1. Введение

Apache Camel — это фреймворк для интеграции приложений и эффективного взаимодействия между различными системами, с чем приходится часто сталкиваться в любой корпоративной инфраструктуре. Apache Camel позволяет разработчику сосредоточиться на логике процессов, не переводя данные в канонический формат, так как Camel поддерживает более 80 API для реализации различных протоколов и типов данных. Таким образом, разработчику — то есть вам — достаточно просто знать, как Camel соединяет все вместе. В этой статье мы по шагам рассмотрим, как интегрировать Apache Camel и Spring Boot.

Но прежде чем обратиться к примеру со средой Spring Boot, разберемся с основными понятиями и терминами Camel.

1.1. Сообщение

Сообщение (message) — это объект данных, используемый системами для обмена информацией друг с другом.

1.2. Обмен

Объект обмена (exchange) — это контейнер, вмещающий сообщение и обеспечивающий взаимодействие между системами. Это контейнер сообщений, определяющий тип обмена сообщениями.

1.3. Контекст Camel

Контекст Camel (CamelContext) — это фундаментальная модель Camel, предоставляющая доступ к различным службам (маршрутам, конечным точкам и др.).

1.4. Маршруты

Маршруты (routes) — это абстракция, позволяющая клиентам и серверам работать независимо. Маршруты, описываемые на доменных языках DSL, представляют собой цепочку вызовов функций (процессоров).

1.5. Предметно-ориентированный язык

Процессоры и конечные точки соединяются друг с другом посредством описания на предметно-ориентированном языке (Domain-Specific Language, DSL) — такие описания и составляют маршруты. В нашем случае язык DSL — это Java Fluent API, но при использовании Camel с другими языками/фреймворками это может быть также XML или какой-то другой язык DSL.

1.6. Процессор

Процессоры (processor) выполняют операции обмена. Маршруты можно представить в качестве логического модуля, который соединяет процессоры, нужные для обработки сообщения.

1.7. Компонент

Компоненты (component) — это модули расширения Apache Camel. Именно они позволяют Camel легко обеспечивать интеграцию с другими системами. Здесь перечислены все основные компоненты, поддерживаемые Camel. Компоненты производят конечные точки с заданным универсальным кодом ресурса (URI).

1.8. Конечная точка

Конечные точки (endpoint) — это точки подключения служб, соединяющих одни системы с другими. Мы создаем конечные точки с помощью компонентов, используя определенный универсальный код ресурса (URI). Например, чтобы создать подключение FTP, в маршруте следует указать следующий URI: ftp://[имяпользователя@]имяхоста[:порт]/директория[?опции] — так компоненты будут создавать конечную точку FTP с заданной конфигурацией.

1.9. Производитель

Производители (producer) — это модули Camel, которые создают сообщения и отправляют их в конечную точку.

1.10. Потребитель

Потребители (consumer) — это модули Camel, которые получают сообщения, созданные производителем, инкапсулируют их внутри объекта exchange и отправляют процессорам.

Пока этих понятий Camel будет достаточно. Изучать их подробно нет необходимости, но базовое представление об архитектуре Camel поможет вам эффективно использовать его возможности. В приведенном ниже примере мы рассмотрим интеграцию этих возможностей в Spring Boot.

2. Общие сведения о приложении

Мы создадим приложение со следующими свойствами:

  • приложение включает объекты данных двух типов: продукты и скидки;

  • продукты мы добавляем при начальной настройке;

  • скидки автоматически применяются к продуктам по истечении определенного времени (Camel Timer + Camel JPA);

  • имеется конечная точка REST для перечисления всех продуктов и скидок (Camel REST);

  • используется документация Swagger (Camel Swagger).

Для этого мы используем H2, Spring Web, Spring JPA и Apache Camel.

3. Начальная настройка приложения

Создайте проект сборки кода со следующими зависимостями. Чтобы подготовить основу для приложения, можно использовать IDE или Spring Initializr. Ниже приведен полный файл pom.xml с пояснениями:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <!--Get required dependencies from a parent-->
   <parent>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-dependencies</artifactId>
      <version>3.3.0</version>
   </parent>
   <artifactId>spring-boot-camel</artifactId>
   <name>spring-boot-camel</name>
   <description>Spring Boot Camel integration tutorial</description>
   <properties>
      <spring-boot-version>2.2.7.RELEASE</spring-boot-version>
      <run.profiles>dev</run.profiles>
   </properties>
   <dependencyManagement>
      <dependencies>
         <!--Import as a pom to let spring-boot to manage spring-boot dependencies version -->
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot-version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
         <!--Import as a pom to let camel manage camel-spring-boot dependencies version-->
         <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-spring-boot-dependencies</artifactId>
            <version>${project.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>
   <dependencies>
      <!--Spring boot dependencies to enable REST, JPA and Core features-->
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
      <!--Camel Spring Boot Dependencies to enable REST, JSON, SWAGGER, JPA features-->
      <dependency>
         <groupId>org.apache.camel.springboot</groupId>
         <artifactId>camel-spring-boot-starter</artifactId>
      </dependency>
      <dependency>
         <groupId>org.apache.camel.springboot</groupId>
         <artifactId>camel-servlet-starter</artifactId>
      </dependency>
      <dependency>
         <groupId>org.apache.camel.springboot</groupId>
         <artifactId>camel-jackson-starter</artifactId>
      </dependency>
      <dependency>
         <groupId>org.apache.camel.springboot</groupId>
         <artifactId>camel-swagger-java-starter</artifactId>
      </dependency>
      <dependency>
         <groupId>org.apache.camel.springboot</groupId>
         <artifactId>camel-jpa-starter</artifactId>
      </dependency>
      <!--In memory database-->
      <dependency>
         <groupId>com.h2database</groupId>
         <artifactId>h2</artifactId>
         <scope>runtime</scope>
      </dependency>
      <!--Spring boot testing-->
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring-boot-version}</version>
            <executions>
               <execution>
                  <goals>
                     <goal>repackage</goal>
                  </goals>
               </execution>
            </executions>
         </plugin>
      </plugins>
   </build>
</project>

4. Подготовка объектов данных

Перед тем как начать работу с Apache Camel, следует подготовить необходимые объекты данных, службы и репозитории.

4.1. Продукт

Создайте объект данных типа «продукт» со следующими полями: идентификатор, имя, цена и скидка. Также мы создадим именованный запрос, который можно вызвать из Camel по имени и получить в ответ результат. Именованный запрос «продукты со скидкой» возвращает все продукты, для которых назначена скидка.

@Entity
@Table(name = "products")
@NamedQuery(name = "discounted-products", query = "select product from Product product where product.discounted IS NOT NULL")
public class Product {

 @Id
 @GeneratedValue
 private int id;

 private String name;

 private Integer price;

 private Integer discounted;

 // Getters and setters
}

Создайте класс ProductRepository, который расширяется из интерфейса CrudRepository репозитория Spring Data. Такое расширение предоставляет в наше распоряжение готовые к вызову запросы: findAll, findById, save и т. д.

public interface ProductRepository extends CrudRepository<Product, Integer> {
}

4.2. Класс службы

Создайте класс ProductService и снабдите его аннотацией службы (service). Чтобы извлечь ProductRepository из контекста Spring, прибегнем к внедрению зависимости через конструктор. Мы предоставляем основные функции findByIdfindAll и save, которые не нуждаются в пояснениях.

@Service
public class ProductService {

    private final ProductRepository products;

    @Autowired
    public ProductService(ProductRepository products) {
        this.products = products;
    }

    public Product findById(Integer id) {
        Optional < Product > product = products.findById(id);
        if (!product.isPresent()) {
            throw new IllegalStateException("Product could not found for given id:" + id);
        }
        return product.get();
    }

    public Iterable < Product > findAll() {
        return products.findAll();
    }

    public void save(Product product) {
        products.save(product);
    }
}

Наконец, создайте файл data.sql в src/main/resources и добавьте три продукта, как показано ниже. При запуске Spring автоматически выполнит data.sql. Подробнее о сценариях инициализации можно прочесть здесь.

INSERT INTO products (id, name, price, discounted)
  VALUES
      (1, 'Book', 25, NULL),
      (2, 'Watch', 100, NULL),
      (3, 'Shoes', 40, NULL);

4.3. Скидка

Создайте объект данных типа «скидка» со следующими полями: идентификатор, величина и продукт. Одномоментно к продукту может применяться только одна скидка, поэтому задайте для поля «продукт» отношение OneToOne.

@Entity
@Table(name = "discounts")
public class Discount {

    @Id
    @GeneratedValue
    private int id;

    private Integer amount;

    @OneToOne
    private Product product;

    // Getters and setters
}

Создайте репозиторий DiscountRepository, как описано выше.

public interface DiscountRepository extends CrudRepository<Discount, Integer> {}

Создайте класс DiscountService аналогично классу ProductService. Наряду с методом findDiscount, который работает аналогично методу findProduct, у нас есть также функция makeDiscount. Эта функция создает случайную скидку, извлекает из базы данных случайный продукт и применяет эту скидку к такому продукту.

@Service
public class DiscountService {

    private final DiscountRepository discounts;
    private final ProductService productService;

    private final Random random = new Random();

    @Autowired
    public DiscountService(DiscountRepository discounts,
        ProductService productService) {
        this.discounts = discounts;
        this.productService = productService;
    }

    public Discount makeDiscount() {
        // create a discount
        Discount discount = new Discount();
        int discountRate = this.random.nextInt(100);
        discount.setAmount(discountRate);

        // select random product
        int productId = this.random.nextInt(3) + 1;
        Product product = productService.findById(productId);

        // set the discount to product and save
        int discountedPrice = product.getPrice() - (discountRate * product.getPrice() / 100);
        product.setDiscounted(discountedPrice);
        productService.save(product);

        discount.setProduct(product);
        return discount;
    }

    public Discount findDiscount(Integer id) {
        Optional < Discount > discount = discounts.findById(id);
        if (!discount.isPresent()) {
            throw new IllegalStateException("Discount could not found for given id:" + id);
        }
        return discount.get();
    }
}

5. Настройка приложения

Создайте application-dev.yml, чтобы настроить сопоставление contextPath для Camel. Добавьте нужные свойства скидок, которые будут использоваться в маршрутах.

camel:
  component:
    servlet:
      mapping:
        contextPath: /javadevjournal/*

discount:
  newDiscountPeriod: 2000
  listDiscountPeriod: 6000/pre>

6. Интеграция Apache Camel

Итак, мы подготовили данные для использования с Apache Camel. Можно приступать к интеграции.

6.1. Создание маршрутов

В Camel имеется базовый класс для создания маршрутов: RouteBuilder. Нам нужно его расширить и снабдить аннотацией @Component. Как уже говорилось выше, Apache Camel использует для создания ссылок на объекты собственный контекст. Однако при работе со SpringBoot сначала Camel выполняет поиск в контексте SpringBoot, а затем внедряет найденные в нем объекты в свой контекст CamelContext, как делает RouteBuilder в нашем примере.

После создания нашего класса маршрутов путем расширения из RouteBuilder необходимо переопределить его метод конфигурирования. Нам нужна логика, которая будет автоматически создавать скидки по истечении заданного периода. Сначала добавим к функции конфигурирования следующий маршрут с пояснением:

@Component
class TimedJobs extends RouteBuilder {

@Override
public void configure() {
        from("timer:new-discount?delay=1000&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 will be added in the next step
}

Опишем нашу работу со Spring Boot в терминологии Camel. Мы создаем маршруты, используя язык Java DSL. Затем мы используем таймер — компонент timer, предоставляемый Camel путем расширения. «Внутри» Camel при этом происходит следующее: при достижении конечной точки таймера (исходя из заданных нами настроек задержки и периода выполнения) запускается производитель.

Прежде чем идти дальше, стоит упомянуть, что Apache Camel поддерживает использование свойств Spring Boot, что мы здесь и делаем. Вы можете ссылаться на такие свойства напрямую, используя имя свойства и значение по умолчанию: {{имя_свойства:значение_по_умолчанию}}.

Теперь определим маршрут make-discount, который должен быть уникальным и на который мы будем ссылаться в дальнейшем. И вызовем функцию makeDiscount в bean-компоненте discountService. Для сообщения выполняется обмен, на который можно сослаться с помощью префикса к основному тексту, и средство ведения журнала потребляет это сообщение, записывая в журнал. Полный список выражений, которые вы можете использовать, приведен в документации по языку Simple. Добавим под имеющимся маршрутом еще один, позволяющий перечислить все продукты с обновленными ценами.

from("jpa:org.apache.camel.example.spring.boot.rest.jpa.Product"
    + "?namedQuery=discounted-products"
    + "&delay={{discount.listDiscountPeriod:6000}}"
    + "&consumeDelete=false")
    .routeId("list-discounted-products")
    .log(
        "Discounted product ${body.name}. Price dropped from ${body.price} to ${body.discounted}");

Мы используем компонент JPA для объекта данных «продукт», вызывая его командой namedQuery. В настройках JPA мы определяем задержку, чтобы программа успела создать какие-то скидки, прежде чем будет сгенерирован список продуктов. Запрос consumeDelete означает, что мы не хотим удалять обработанный объект данных «продукт». Полный список возможных настроек представлен на странице компонента JPA. Вот так выглядят журналы нашего задания:

Created %27 discount for Watch
Created %84 discount for Book
Created %92 discount for Shoes
Discounted product Book. Price dropped from 25 to 4
Discounted product Watch. Price dropped from 100 to 73
Discounted product Shoes. Price dropped from 40 to 4

6.2. Создание конечных точек REST

Мы уже настроили таймер для запуска функций. Теперь выполним интеграцию с конечными точками REST и создадим документацию Swagger. Создадим новый маршрут путем расширения RouteBuilder. Для настройки нашего приложения необходимо вызвать функцию Camel restConfiguration.

@Component
class RestApi extends RouteBuilder {

@Override
public void configure() {
        restConfiguration()
            .contextPath("/javadevjournal")
            .apiContextPath("/api-doc")
            .apiProperty("api.title", "JAVA DEV JOURNAL REST API")
            .apiProperty("api.version", "1.0")
            .apiProperty("cors", "true")
            .apiContextRouteId("doc-api")
            .port(env.getProperty("server.port", "8080"))
            .bindingMode(RestBindingMode.json);

        rest("/products").description("Details of products")
            .get("/").description("List of all products")
            .route().routeId("products-api")
            .bean(ProductService.class, "findAll")
            .endRest()
            .get("discounts/{id}").description("Discount of a product")
            .route().routeId("discount-api")
            .bean(DiscountService.class, "findDiscount(${header.id})");
    }
}

Указываем путь contextPath — javadevjournal — и контекст-путь API — api-doc, который используется для Swagger. По умолчанию режим привязки отключен. Поскольку мы добавили Jackson JSON в файл pom.xml, можно использовать формат привязки JSON. Здесь приведен полный список возможных конфигураций. Во второй части нашей конфигурации определяем конечную точку /products и возвращаемый результат ProductService.findAll. Расширяем конечную точку /products, добавляя к ней /discounts/{id}, и вызываем функцию DiscountService.findDiscount с идентификатором id, полученным в ответ на запрос. {header} относится к входящим данным, которые упоминались выше в разделе о языке Simple, для заполнителя {body}.

Перейдя по адресу http://localhost:8080/javadevjournal/api-doc, вы получите ответ Swagger. Введите http://localhost:8080/javadevjournal/products, и вы получите:

[
    {
        "id": 1,
        "name": "Book",
        "price": 25,
        "discounted": 4
    },
    {
        "id": 2,
        "name": "Watch",
        "price": 100,
        "discounted": 73
    },
    {
        "id": 3,
        "name": "Shoes",
        "price": 40,
        "discounted": 4
    }
]

Аналогичным образом, перейдя по ссылке http://localhost:8080/javadevjournal/products/discounts/1, вы получите:

{
    "id": 1,
    "amount": 92,
    "product": {
        "id": 3,
        "name": "Shoes",
        "price": 40,
        "discounted": 4
    }
}

Резюме

В этой статье мы рассмотрели, как интегрировать Apache Camel со Spring Boot. Мы кратко описали, что представляет из себя Apache Camel и как его можно интегрировать со Spring Boot на примере реалистичных сценариев. Исходный код приложения доступен на Github.


Перевод статьи подготовлен в преддверии старта курса "Разработчик на Spring Framework".

14 мая пройдет demo day курса - это отличная возможность задать все вопросы по курсу, узнать подробнее о программе курса, особенностях онлайн-формата, навыках, компетенциях и перспективах, которые ждут выпускников после обучения.

ЗАПИСАТЬСЯ НА DEMO DAY