Статьи

Apache Camel — разработка приложения с нуля (часть 1/2)

Прежде чем мы начнем

Некоторое время назад я написал учебник по Spring Integration, чтобы продемонстрировать, как использовать Spring Integration в примере приложения, основанном на реальной системе обработки счетов. Я получил довольно положительные отзывы об этом, поэтому я решил, что покажу вам, как мы можем создать то же самое приложение, используя Apache Camel — величайшего конкурента Spring Integration.

Приложение, которое мы собираемся создать, будет почти таким же, как и текст этого урока. Новые части будут в основном посвящены Apache Camel и его использованию.

Вы можете либо следовать пошаговому руководству шаг за шагом и самостоятельно создавать приложение, либо вы можете продолжить и получить код с github: СКАЧАТЬ ИСТОЧНИКИ ЗДЕСЬ: https://github.com/vrto/apache-camel-invoices

Обратите внимание, что есть и другие способы написания «маршрутов» Camel — Java DSL, Spring XML, Scala DSL… В этом руководстве я буду использовать Java DSL , но для тех, кто заинтересован, вы также можете найти тот же проект, настроенный через Spring XML, здесь , Какой бы путь вы ни выбрали, пора начинать!

Приложение для обработки счетов — функциональное описание

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

Apache Camel — это интегрированная среда — это означает, что она предоставляет содержательные абстракции для сложных систем, которые вы интегрируете.
Для этого урока есть много важных понятий. Позвольте мне подвести их итог для вас:

  • Верблюжий контекст : система времени выполнения, которая объединяет все части.
  • Сообщение : фундаментальная сущность — основной принцип обмена сообщениями. Он состоит из заголовков , тела и вложений .
  • Exchange : Контейнер для сообщений (абстракция того, что фактически отправлено по системе). Содержит сообщения In и, опционально, Out .
  • Маршрут : Цепочка процессоров. Если вам нравится более «академическое» объяснение, тогда route — это график, где «узел» представлен некоторым процессором, а «линия» представлена ​​некоторым каналом.
  • Процессор : использует / изменяет входящий обмен. Выход одного процессора подключен к входу другого.
  • Конечная точка : моделирует конец канала. Настроен с использованием URI. Пример:
    1
    file:data/inbox?delay=5000
  • Компонент : Фабрика для конечных точек. Обращается с префиксами (jms :, file: и т. Д.).

Если вы хотите узнать больше информации об основных концепциях верблюда, я настоятельно рекомендую вам прочитать очень хорошую книгу « Верблюд в действии» .

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

интеграции счет-фактура

На рисунке вы видите маршрут, который иллюстрирует нашу структуру обмена сообщениями и основные компоненты системы — они отмечены красными цифрами. Давайте пройдемся по ним (мы вернемся к каждому компоненту более подробно позже):

  1. Invoices Gateway — это место, где мы будем выставлять новые счета, чтобы они могли перейти на уровень обмена сообщениями.
  2. Splitter — система предназначена для приема коллекции счетов, но нам нужно будет обрабатывать каждый счет отдельно. Более конкретно, сообщение с телом типа «Коллекция» будет разделено на несколько сообщений, где каждое сообщение будет иметь индивидуальный счет в качестве тела.
  3. Фильтр — наша система предназначена для автоматической обработки только тех счетов, которые выдают менее 10 000 долларов США.
  4. Маршрутизатор на основе содержимого — в некоторых счетах используются номера счетов IBAN, и у нас есть две разные учетные записи: одна для локальных транзакций и одна для иностранных транзакций. Задача компонента маршрутизатора — отправить сообщение, содержащее счет-фактуру, на правильный канал — либо для локальных счетов, либо для внешних счетов.
  5. Трансформаторы. Несмотря на то, что мы принимаем счета в систему, наши банковские API работают с другими типами — Платежами. Работа компонента-преобразователя состоит в том, чтобы принять какое-то сообщение и преобразовать его в другое сообщение в соответствии с предоставленной логикой. Мы хотим преобразовать полезную нагрузку исходного сообщения (инвойса) в новую полезную нагрузку — платеж.
  6. Активатор банковского обслуживания — после того, как мы обработали счета и произвели некоторые фактические платежи, мы готовы поговорить с внешней банковской системой. Мы показали обслуживание таких систем, и когда сообщение, несущее платеж, поступает в правильный (банковский) канал, мы хотим активировать некоторую логику — передать платеж банку и позволить банку выполнить дальнейшую обработку.

Создание проекта

К настоящему времени у вас должен быть общий обзор того, что делает система и как она структурирована. Перед тем, как мы начнем писать код, вам понадобится настоящий проект Maven, а также настройка структуры и требуемых зависимостей. Если вы знакомы с Maven, посмотрите файл pom.xml ниже, в противном случае, если вы хотите сэкономить время, вы можете использовать шаблон проекта, который я создал для вас: скачайте шаблон проекта Maven .

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?xml version="1.0" encoding="UTF-8"?>
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>apache-camel-invoices</groupId>
    <artifactId>apache-camel-invoices</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <camel-version>2.12.0</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-spring</artifactId>
            <version>${camel-version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-quartz</artifactId>
            <version>2.9.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-test-spring</artifactId>
            <version>${camel-version}</version>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>14.0.1</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <!--
 
            - uncomment when InvoicesApplication class will exist
 
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>com.vrtoonjava.invoices.InvoicesApplication</mainClass>
                </configuration>
            </plugin>
            -->
        </plugins>
    </build>
 
</project>

Самонаводящийся верблюд

Перед использованием Apache Camel нам нужно немного его загрузить. Сначала мы создадим пакет com.vrtoonjava.routes который мы поместим наши конфигурации маршрутов. Во-вторых, мы создадим класс InvoicesRouteBuilder который будет местом для настройки наших маршрутов. Итак, создайте класс так:

01
02
03
04
05
06
07
08
09
10
11
12
13
package com.vrtoonjava.routes;
 
import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;
 
@Component
public class InvoicesRouteBuilder extends RouteBuilder {
 
    @Override
    public void configure() throws Exception {
        //TODO configure route
    }
}

Последнее, что нужно сделать, это подключить Camel к Spring (Camel будет использовать Spring в качестве реестра для bean-компонентов). Добавьте файл camel-config.xml в папку src / main / resources со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
 
    <camelContext xmlns="http://camel.apache.org/schema/spring">
        <routeBuilder ref="invoicesRouteBuilder"/>
    </camelContext>
 
</beans>

Давайте теперь рассмотрим шесть основных компонентов системы более подробно и разберемся с реальным кодом.

1. Шлюз счетов

Во-первых, давайте посмотрим код для Invoice — который будет одним из основных классов в нашей системе. Я буду использовать пакет com.vrtoonjava качестве корневого пакета, а invoices и banking качестве com.vrtoonjava :

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
package com.vrtoonjava.invoices;
 
import com.google.common.base.Objects;
 
import java.math.BigDecimal;
 
public class Invoice {
 
    private final String iban;
    private final String address;
    private final String account;
    private final BigDecimal dollars;
 
    public Invoice(String iban, String address, String account, BigDecimal dollars) {
        this.iban = iban;
        this.address = address;
        this.account = account;
        this.dollars = dollars;
    }
 
    public boolean isForeign() {
        return null != iban && !iban.isEmpty();
    }
 
    public String getAddress() {
        return address;
    }
 
    public String getAccount() {
        return account;
    }
 
    public BigDecimal getDollars() {
        return dollars;
    }
 
    public String getIban() {
        return iban;
    }
 
    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                .add("iban", iban)
                .add("address", address)
                .add("account", account)
                .add("dollars", dollars)
                .toString();
    }
 
}

Представьте, что мы получаем счета из другой системы (будь то база данных, веб-сервис или что-то еще), но мы не хотим связывать эту часть с уровнем интеграции. Мы будем использовать компонент Gateway для этой цели. Шлюз вводит контракт, который отделяет клиентский код от уровня интеграции (в нашем случае это зависимости Apache Camel). Давайте посмотрим код для InvoiceCollectorGateway :

01
02
03
04
05
06
07
08
09
10
11
12
package com.vrtoonjava.invoices;
 
import java.util.Collection;
 
/**
 * Defines a contract that decouples client from the Apache Camel framework.
 */
public interface InvoiceCollectorGateway {
 
    void collectInvoices(Collection<Invoice> invoices);
 
}

Мы определили этот интерфейс для того, чтобы клиент зависел только от контракта. Фактическая реализация будет зависеть от Camel, и мы можем создать ее следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.vrtoonjava.invoices;
 
import org.apache.camel.Produce;
import org.apache.camel.ProducerTemplate;
import org.springframework.stereotype.Component;
 
import java.util.Collection;
 
@Component
public class CamelInvoiceCollectorGateway implements InvoiceCollectorGateway {
 
    @Produce(uri = "seda:newInvoicesChannel")
    ProducerTemplate producerTemplate;
 
    @Override
    public void collectInvoices(Collection<Invoice> invoices) {
        producerTemplate.sendBody(invoices);
    }
 
}

Обратите внимание на аннотацию @Produce . Эта аннотация сообщает Camel, что поле seda:newInvoicesChannel является производителем для конечной точки seda:newInvoicesChannel . Когда клиент вызывает метод collectInvoices , шлюз отправит новое сообщение (содержащее тело списка) на seda:newInvoicesChannel . Это оставляет клиента отделенным от средств обмена сообщениями, но позволяет нам поместить результат в реальный канал обмена сообщениями.

Почему СЕДА?

Обычно при построении систем обмена сообщениями в Java вы используете что-то вроде JMS, которое предоставляет множество полезных функций, таких как высокая надежность или постоянство сообщений. В этом уроке мы будем использовать более легкую альтернативу на основе компонента SEDA — он позволяет нам использовать асинхронный обмен сообщениями в памяти с практически нулевой конфигурацией.

2. Счет Разделитель

Со Шлюза мы отправляем одно большое сообщение в систему, которое содержит коллекцию счетов-фактур, другими словами — Сообщение имеет тело типа Collection . Поскольку мы хотим обрабатывать счета по отдельности, мы получим результат из seda:newInvoicesChannel и используем компонент сплиттера, который создаст несколько сообщений. Каждое из этих новых сообщений будет иметь тело типа Invoice. Затем мы разместим сообщения на новый канал — seda:singleInvoicesChannel . Вот как мы определяем сплиттер (добавьте следующий код в метод configure вашего InvoicesRouteBuilder ):

1
2
3
4
from("seda:newInvoicesChannel")
        .log(LoggingLevel.INFO, "Invoices processing STARTED")
        .split(body())
        .to("seda:singleInvoicesChannel");

3. Фильтрация некоторых счетов

Бизнес-пример использования нашей системы требует от нас автоматической обработки только тех счетов, которые выдают нам менее 10 000 долларов США. Для этого мы введем компонент Filter . Мы будем seda:singleInvoicesChannel сообщения из seda:singleInvoicesChannel , применять к ним нашу логику фильтрации, а затем записывать сопоставленные результаты в новый seda:filteredInvoicesChannel . В Apache Camel вы можете подключать свои собственные предикаты, которые содержат логику фильтрации. Во-первых, давайте определим такой предикат (расширив org.apache.camel.Predicate Camel):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.vrtoonjava.invoices;
 
import org.apache.camel.Exchange;
import org.apache.camel.Predicate;
 
public class LowEnoughAmountPredicate implements Predicate {
 
    public static final int LOW_ENOUGH_THRESHOLD = 10_000;
 
    @Override
    public boolean matches(Exchange exchange) {
        Invoice invoice = exchange.getIn().getBody(Invoice.class);
        boolean lowEnough = invoice.getDollars().intValue() < LOW_ENOUGH_THRESHOLD;
        System.out.println("Amount of $" + invoice.getDollars()
                + (lowEnough ? " can" : " can not") + " be automatically processed by system");
 
        return lowEnough;
    }
 
}

Для краткости я не буду вставлять юнит-тесты в этом руководстве, но если вам интересно, скачайте проект github и посмотрите сами тесты .

Теперь нам нужно подключить этот предикат к нашему маршруту, поэтому добавьте следующий код в ваш метод configure :

1
2
3
from("seda:singleInvoicesChannel")
        .filter(new LowEnoughAmountPredicate())
        .to("seda:filteredInvoicesChannel");

4. Маршрутизация счетов

Пока что мы разделили и отфильтровали некоторые счета. Теперь пришло время более тщательно проверить содержание каждого счета и решить, является ли он счетом, выданным из текущей страны (местная) или из другой страны (иностранная). Apache Camel позволяет использовать определение Content-Based Router с помощью метода choice() в Java DSL. Мы даже можем напрямую получить доступ к телу сообщения в Java DSL и выполнить несколько простых оценок. Добавьте следующий код в ваш метод configure (и обратите внимание, как мы получаем доступ к телу с помощью стандартного выражения ${body.isForeign} ):

1
2
3
4
5
6
from("seda:filteredInvoicesChannel")
        .choice()
            .when().simple("${body.isForeign}")
                .to("seda:foreignInvoicesChannel")
            .otherwise()
                .to("seda:localInvoicesChannel");

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

Справка: Apache Camel — разработка приложения с нуля (часть 1/2) от нашего партнера по JCG Михала Вртиака из блога vrtoonjava .