Статьи

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

Это вторая часть руководства, где мы создаем приложение для обработки счетов с использованием Apache Camel. Если вы пропустили это, обязательно посмотрите первую часть . Ранее мы определили функциональные требования для системы, создали шлюз, разделитель, фильтр и компонентный маршрутизатор. Давайте продолжим создавать трансформатор.

5. Преобразование счетов в платежи

Мы успешно отфильтровали «слишком дорогие» счета из системы (они могут нуждаться в ручной проверке или около того). Важно то, что теперь мы можем взять счет и произвести оплату из него. Для начала добавим класс Payment в banking пакет:

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
package com.vrtoonjava.banking;
 
import com.google.common.base.Objects;
 
import java.math.BigDecimal;
 
public class Payment {
 
    private final String senderAccount;
    private final String receiverAccount;
    private final BigDecimal dollars;
 
    public Payment(String senderAccount, String receiverAccount, BigDecimal dollars) {
        this.senderAccount = senderAccount;
        this.receiverAccount = receiverAccount;
        this.dollars = dollars;
    }
 
    public String getSenderAccount() {
        return senderAccount;
    }
 
    public String getReceiverAccount() {
        return receiverAccount;
    }
 
    public BigDecimal getDollars() {
        return dollars;
    }
 
    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                .add("senderAccount", senderAccount)
                .add("receiverAccount", receiverAccount)
                .add("dollars", dollars)
                .toString();
    }
 
}

Поскольку у нас будет два способа создания платежа (из местных и иностранных счетов), давайте определим общий контракт (интерфейс) для создания платежей. Поместите интерфейс PaymentCreator в banking пакет:

01
02
03
04
05
06
07
08
09
10
11
12
13
package com.vrtoonjava.banking;
 
import com.vrtoonjava.invoices.Invoice;
 
/**
 * Creates payment for bank from the invoice.
 * Real world implementation might do some I/O expensive stuff.
 */
public interface PaymentCreator {
 
    Payment createPayment(Invoice invoice) throws PaymentException;
 
}

Технически это простая параметризованная фабрика. Обратите внимание, что это вызывает PaymentException . Мы PaymentException к обработке исключений позже, но вот код для простого PaymentException :

1
2
3
4
5
6
7
8
9
package com.vrtoonjava.banking;
 
public class PaymentException extends Exception {
 
    public PaymentException(String message) {
        super(message);
    }
 
}

Теперь мы можем добавить две реализации в пакет invoices . Сначала давайте создадим класс LocalPaymentCreator :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.vrtoonjava.invoices;
 
import com.vrtoonjava.banking.Payment;
import com.vrtoonjava.banking.PaymentCreator;
import com.vrtoonjava.banking.PaymentException;
import org.springframework.stereotype.Component;
 
@Component
public class LocalPaymentCreator implements PaymentCreator {
 
    // hard coded account value for demo purposes
    private static final String CURRENT_LOCAL_ACC = "current-local-acc";
 
    @Override
    public Payment createPayment(Invoice invoice) throws PaymentException {
        if (null == invoice.getAccount()) {
            throw new PaymentException("Account can not be empty when creating local payment!");
        }
 
        return new Payment(CURRENT_LOCAL_ACC, invoice.getAccount(), invoice.getDollars());
    }
 
}

Другим создателем будет ForeignPaymentCreator с довольно простой реализацией:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.vrtoonjava.invoices;
 
import com.vrtoonjava.banking.Payment;
import com.vrtoonjava.banking.PaymentCreator;
import com.vrtoonjava.banking.PaymentException;
import org.springframework.stereotype.Component;
 
@Component
public class ForeignPaymentCreator implements PaymentCreator {
 
    // hard coded account value for demo purposes
    private static final String CURRENT_IBAN_ACC = "current-iban-acc";
 
    @Override
    public Payment createPayment(Invoice invoice) throws PaymentException {
        if (null == invoice.getIban()) {
            throw new PaymentException("IBAN mustn't be null when creating foreign payment!");
        }
 
        return new Payment(CURRENT_IBAN_ACC, invoice.getIban(), invoice.getDollars());
    }
 
}

Эти два создателя — простые бобы Spring, и Apache Camel предоставляет действительно хороший способ соединить их с маршрутом. Мы будем создавать два преобразователя с помощью метода transform() в Java DSL Camel. Мы подключим правильные преобразователи к seda:foreignInvoicesChannel и seda:localInvoicesChannel и заставим их перенаправлять результаты в seda:bankingChannel . Добавьте следующий код в ваш метод configure :

1
2
3
4
5
6
7
from("seda:foreignInvoicesChannel")
        .transform().method("foreignPaymentCreator", "createPayment")
        .to("seda:bankingChannel");
 
from("seda:localInvoicesChannel")
        .transform().method("localPaymentCreator", "createPayment")
        .to("seda:bankingChannel");

6. Передача платежей в банковскую службу (Service Activator)

Платежи готовы, и сообщения, содержащие их, ожидают в seda:bankingChannel . Последний шаг процесса — использование компонента Service Activator . Это работает просто: когда в канале появляется новое сообщение, Apache Camel вызывает логику, указанную в компоненте Service Activator. Другими словами, мы подключаем внешний сервис к нашей существующей инфраструктуре обмена сообщениями.
Для этого нам сначала нужно увидеть договор на банковское обслуживание. Поэтому поместите интерфейс BankingService в banking пакет (в реальном мире это, вероятно, будет находиться в каком-то внешнем модуле):

01
02
03
04
05
06
07
08
09
10
package com.vrtoonjava.banking;
 
/**
 * Contract for communication with bank.
 */
public interface BankingService {
 
    void pay(Payment payment) throws PaymentException;
 
}

Теперь нам понадобится актуальная реализация BankingService . Опять же, очень маловероятно, что реализация будет находиться в нашем проекте (вероятно, это будет служба, предоставляемая удаленно), но давайте, по крайней мере, создадим некоторую фиктивную реализацию для целей учебника. Добавьте класс MockBankingService в banking пакет:

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
package com.vrtoonjava.banking;
 
import org.springframework.stereotype.Service;
 
import java.util.Random;
 
/**
 * Mock service that simulates some banking behavior.
 * In real world, we might use some web service or a proxy of real service.
 */
@Service
public class MockBankingService implements BankingService {
 
    private final Random rand = new Random();
 
    @Override
    public void pay(Payment payment) throws PaymentException {
        if (rand.nextDouble() > 0.9) {
            throw new PaymentException("Banking services are offline, try again later!");
        }
 
        System.out.println("Processing payment " + payment);
    }
 
}

Ложная реализация создает в некоторых случайных случаях (~ 10%) сбой. Конечно, для лучшего разделения мы не будем использовать его напрямую, вместо этого мы создадим зависимость от нашего пользовательского компонента от контракта (интерфейса). Давайте теперь добавим класс PaymentProcessor в пакет invoices :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.vrtoonjava.invoices;
 
import com.vrtoonjava.banking.BankingService;
import com.vrtoonjava.banking.Payment;
import com.vrtoonjava.banking.PaymentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
/**
 * Endpoint that picks Payments from the system and dispatches them to the
 * service provided by bank.
 */
@Component
public class PaymentProcessor {
 
    @Autowired
    BankingService bankingService;
 
    public void processPayment(Payment payment) throws PaymentException {
        bankingService.pay(payment);
    }
 
}

Apache Camel предоставляет простой способ вызова метода для произвольного компонента, когда сообщение приходит к определенной конечной точке ( EIP описывает это как Service Activator), используя метод bean() в Java DSL Camel:

1
2
from("seda:bankingChannel")
        .bean(PaymentProcessor.class, "processPayment");

Обработка ошибок

Одна из самых больших проблем систем обмена сообщениями — правильно идентифицировать и обрабатывать ошибки. EAI описывает множество подходов, и мы будем использовать реализацию EIP Dead Letter Channel . Dead Letter Channel — это просто еще один канал, и мы можем предпринять соответствующие действия, если в этом канале появляется сообщение об ошибке. В реальных приложениях мы, вероятно, пойдем на логику повторов или профессиональную отчетность, в нашем учебном примере мы просто распечатаем причину ошибки. Давайте errorHandler() ранее определенный Service Activator и подключим компонент errorHandler() . Когда PaymentProcessor генерирует исключение, этот errorHandler будет пересылать исходное сообщение, вызвавшее ошибку, в Dead Letter Channel:

1
2
3
from("seda:bankingChannel")
        .errorHandler(deadLetterChannel("log:failedPayments"))
        .bean(PaymentProcessor.class, "processPayment");

Наконец, вот последний и полный маршрут:

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
package com.vrtoonjava.routes;
 
import com.vrtoonjava.invoices.LowEnoughAmountPredicate;
import com.vrtoonjava.invoices.PaymentProcessor;
import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;
 
@Component
public class InvoicesRouteBuilder extends RouteBuilder {
 
    @Override
    public void configure() throws Exception {
        from("seda:newInvoicesChannel")
                .log(LoggingLevel.INFO, "Invoices processing STARTED")
                .split(body())
                .to("seda:singleInvoicesChannel");
 
        from("seda:singleInvoicesChannel")
                .filter(new LowEnoughAmountPredicate())
                .to("seda:filteredInvoicesChannel");
 
        from("seda:filteredInvoicesChannel")
                .choice()
                    .when().simple("${body.isForeign}")
                        .to("seda:foreignInvoicesChannel")
                    .otherwise()
                        .to("seda:localInvoicesChannel");
 
        from("seda:foreignInvoicesChannel")
                .transform().method("foreignPaymentCreator", "createPayment")
                .to("seda:bankingChannel");
 
        from("seda:localInvoicesChannel")
                .transform().method("localPaymentCreator", "createPayment")
                .to("seda:bankingChannel");
 
        from("seda:bankingChannel")
                .errorHandler(deadLetterChannel("log:failedPayments"))
                .bean(PaymentProcessor.class, "processPayment");
    }
 
}

Запуск всего этого

Теперь мы создадим работу, которая будет (по фиксированной ставке) отправлять новые счета в систему. Это только стандартный компонент Spring, использующий аннотацию Spring @Scheduled . Итак, давайте добавим новый класс — InvoicesJob в проект:

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
package com.vrtoonjava.invoices;
 
import org.apache.camel.Produce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
 
@Component
public class InvoicesJob {
 
    private int limit = 10; // default value, configurable
 
    @Autowired
    InvoiceCollectorGateway invoiceCollector;
 
    @Autowired
    InvoiceGenerator invoiceGenerator;
 
    @Scheduled(fixedRate = 4000)
    public void scheduleInvoicesHandling() {
        Collection<Invoice> invoices = generateInvoices(limit);
        System.out.println("\n===========> Sending " + invoices.size() + " invoices to the system");
        invoiceCollector.collectInvoices(invoices);
    }
 
    // configurable from Injector
    public void setLimit(int limit) {
        this.limit = limit;
    }
 
    private Collection<Invoice> generateInvoices(int limit) {
        List<Invoice> invoices = new ArrayList<>();
        for (int i = 0; i < limit; i++) {
            invoices.add(invoiceGenerator.nextInvoice());
        }
 
        return invoices;
    }
}

Задание вызывает (каждые 4 секунды) InvoicesGenerator и пересылает счета в шлюз (первый компонент, о котором мы читаем). Чтобы это работало, нам также нужен класс InvoicesGenerator :

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
package com.vrtoonjava.invoices;
 
import org.springframework.stereotype.Component;
 
import java.math.BigDecimal;
import java.util.Random;
 
/**
 * Utility class for generating invoices.
 */
@Component
public class InvoiceGenerator {
 
    private Random rand = new Random();
 
    public Invoice nextInvoice() {
        return new Invoice(rand.nextBoolean() ? iban() : null, address(), account(), dollars());
    }
 
    private BigDecimal dollars() {
        return new BigDecimal(1 + rand.nextInt(20_000));
    }
 
    private String account() {
        return "test-account " + rand.nextInt(1000) + 1000;
    }
 
    private String address() {
        return "Test Street " + rand.nextInt(100) + 1;
    }
 
    private String iban() {
        return "test-iban-" + rand.nextInt(1000) + 1000;
    }
 
}

Это всего лишь простое фиктивное средство, которое позволит нам увидеть, как работает система. В реальном мире мы бы не использовали какой-либо генератор, но, вероятно, вместо этого использовали какой-то открытый сервис.

Теперь в папке resources создайте новый весенний конфигурационный файл — invoices-context.xml и объявите поддержку сканирования компонентов и планирования задач:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
       xmlns:context = "http://www.springframework.org/schema/context"
 
    <import resource = "camel-config.xml" />
 
    <context:component-scan base-package = "com.vrtoonjava" />
 
    <task:executor id = "executor" pool-size="10" />
    <task:scheduler id = "scheduler" pool-size="10" />
    <task:annotation-driven executor="executor" scheduler="scheduler" />
 
</beans>

Чтобы увидеть, как все это работает, нам понадобится еще одна последняя часть — стандартное основное Java-приложение, в котором мы создадим SpringConfision Application.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package com.vrtoonjava.invoices;
 
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
/**
 * Entry point of the application.
 * Creates Spring context, lets Spring to schedule job and use schema.
 */
public class InvoicesApplication {
 
    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("/invoices-context.xml");
    }
 
}

Просто запустите mvn clean install из командной строки и запустите метод main в классе InvoicesApplication. Вы должны увидеть похожую информацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
===========> Sending 10 invoices to the system
13:48:54.347 INFO  [Camel (camel-1) thread #0 - seda://newInvoicesChannel][route1] Invoices processing STARTED
Amount of $4201 can be automatically processed by system
Amount of $15110 can not be automatically processed by system
Amount of $17165 can not be automatically processed by system
Amount of $1193 can be automatically processed by system
Amount of $6077 can be automatically processed by system
Amount of $17164 can not be automatically processed by system
Amount of $11272 can not be automatically processed by system
Processing payment Payment{senderAccount=current-local-acc, receiverAccount=test-account 1901000, dollars=4201}
Amount of $3598 can be automatically processed by system
Amount of $14449 can not be automatically processed by system
Processing payment Payment{senderAccount=current-local-acc, receiverAccount=test-account 8911000, dollars=1193}
Amount of $12486 can not be automatically processed by system
13:48:54.365 INFO  [Camel (camel-1) thread #5 - seda://bankingChannel][failedPayments] Exchange[ExchangePattern: InOnly, BodyType: com.vrtoonjava.banking.Payment, Body: Payment{senderAccount=current-iban-acc, receiverAccount=test-iban-7451000, dollars=6077}]
Processing payment Payment{senderAccount=current-iban-acc, receiverAccount=test-iban-6201000, dollars=3598}

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