Это вторая часть руководства, где мы создаем приложение для обработки счетов с использованием 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;@Componentpublic 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;@Componentpublic 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. */@Servicepublic 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. */@Componentpublic 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;@Componentpublic 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;@Componentpublic 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. */@Componentpublic 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:xsi = "http://www.w3.org/2001/XMLSchema-instance" xmlns:task = "http://www.springframework.org/schema/task" xsi:schemaLocation = "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <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 system13:48:54.347 INFO [Camel (camel-1) thread #0 - seda://newInvoicesChannel][route1] Invoices processing STARTEDAmount of $4201 can be automatically processed by systemAmount of $15110 can not be automatically processed by systemAmount of $17165 can not be automatically processed by systemAmount of $1193 can be automatically processed by systemAmount of $6077 can be automatically processed by systemAmount of $17164 can not be automatically processed by systemAmount of $11272 can not be automatically processed by systemProcessing payment Payment{senderAccount=current-local-acc, receiverAccount=test-account 1901000, dollars=4201}Amount of $3598 can be automatically processed by systemAmount of $14449 can not be automatically processed by systemProcessing payment Payment{senderAccount=current-local-acc, receiverAccount=test-account 8911000, dollars=1193}Amount of $12486 can not be automatically processed by system13: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} |