Это вторая часть руководства, где мы создаем приложение для обработки счетов с использованием 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: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 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} |