Это вторая часть руководства, где мы создаем приложение для обработки счетов, используя Spring Integration. Если вы пропустили это, обязательно посмотрите первую часть . Ранее мы определили функциональные требования для системы, создали шлюз, разделитель, фильтр и компонент маршрутизатора. Давайте продолжим создавать трансформатор.
5. Преобразование счетов в платежи
Мы успешно отфильтровали «слишком дорогие» счета из системы (они могут нуждаться в ручной проверке или около того). Важно то, что теперь мы можем взять счет и произвести оплату из него. Во-первых, давайте добавим класс оплаты в банковской
пакет:
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 в банковский пакет:
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 :
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
24
25
|
package com.vrtoonjava.invoices; import com.vrtoonjava.banking.Payment; import com.vrtoonjava.banking.PaymentCreator; import com.vrtoonjava.banking.PaymentException; import org.springframework.integration.annotation.Transformer; 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 @Transformer 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
24
25
|
package com.vrtoonjava.invoices; import com.vrtoonjava.banking.Payment; import com.vrtoonjava.banking.PaymentCreator; import com.vrtoonjava.banking.PaymentException; import org.springframework.integration.annotation.Transformer; 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 @Transformer 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()); } } |
Интересная часть о создателях — аннотация @Transformer . Это похоже на концепцию, которую мы использовали с аннотацией @Filter — только в этот раз мы сообщаем Spring Integration, что ей следует использовать этот метод для логики преобразования полезной нагрузки. В любом случае мы будем использовать внешний или локальный преобразователь, поэтому новое сообщение закончится в канале bankingChannel . Давайте определим эти новые преобразователи в нашем файле схемы:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
< int:transformer input-channel = 'localTransactions' output-channel = 'bankingChannel' ref = 'localPaymentCreator' /> < int:transformer input-channel = 'foreignTransactions' output-channel = 'bankingChannel' ref = 'foreignPaymentCreator' /> < int:channel id = 'bankingChannel' > < int:queue capacity = '1000' /> </ int:channel > |
6. Передача платежей в банковскую службу (Service Activator)
Платежи готовы, и сообщения, содержащие их, ждут в банковском канале. Последний шаг процесса — использование компонента Service Activator . Это работает просто: когда в канале появляется новое сообщение, Spring Integration вызывает логику, указанную в компоненте Service Activator. Поэтому, когда в банковском канале появляется новый платеж, мы хотим передать его в банковскую службу.
Для этого нам сначала нужно увидеть договор на банковское обслуживание. Поэтому поместите интерфейс BankingService в банковский пакет (в реальном мире это, вероятно, будет находиться в каком-то внешнем модуле)
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 в банковский пакет:
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 в пакет счетов :
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.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.integration.annotation.ServiceActivator; 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; @ServiceActivator public void processPayment(Payment payment) throws PaymentException { bankingService.pay(payment); } } |
Снова — обратите внимание на аннотацию @ServiceActivator . Это означает, что Spring Integration должен вызывать соответствующий метод, когда компонент-активатор службы входит в игру. Чтобы использовать активатор сервиса, нам нужно добавить его в схему интеграции:
1
2
3
4
5
|
< int:service-activator input-channel = 'bankingChannel' ref = 'paymentProcessor' > < int:poller fixed-rate = '500' error-channel = 'failedPaymentsChannel' /> </ int:service-activator > < int:channel id = 'failedPaymentsChannel' /> |
Обратите внимание, что мы определяем атрибут с фиксированной скоростью, который означает, что активатор будет вызываться каждые полсекунды (если в банковском канале присутствует какое-то сообщение). Мы также определяем атрибут error-channel , но мы доберемся до него только сейчас.
Обработка ошибок
Одна из самых больших проблем систем обмена сообщениями — правильно идентифицировать и обрабатывать ошибки. Spring Integration предоставляет технику, называемую «каналы ошибок», где мы (очевидно) можем отправлять сообщения об ошибках из системы. Канал ошибок — это просто еще один канал, и мы можем предпринять соответствующие действия, когда в этом канале появляется сообщение об ошибке. В реальных приложениях мы, вероятно, пойдем на логику повторов или профессиональную отчетность, в нашем учебном примере мы просто распечатаем причину ошибки. В предыдущем компоненте (Service Activator) мы указали свойство error-channel для ссылки на failedPaymentsChannel . Когда сообщение поступит на этот канал, мы вызовем другого активатора службы и распечатаем ошибку. Вот реализация активатора службы FailedPaymentHandler :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
package com.vrtoonjava.invoices; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.stereotype.Component; @Component public class FailedPaymentHandler { @ServiceActivator public void handleFailedPayment(Exception e) { System.out.println( 'Payment failed: ' + e); // now the system should do something reasonable, like retrying the payment // omitted for the tutorial purposes } } |
И давайте подключим его к схеме интеграции как обычно:
1
2
3
|
< int:service-activator input-channel = 'failedPaymentsChannel' ref = 'failedPaymentHandler' /> |
Запуск всего этого
Теперь мы создадим работу, которая будет (по фиксированной ставке) отправлять новые счета в систему. Это только стандартный компонент 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
44
45
46
47
|
package com.vrtoonjava.invoices; 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; /** * Job that every n-seconds generates invoices and sends them to the system. * In real world this might be endpoint receiving invoices from another system. */ @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 ; } } |
Это всего лишь простое фиктивное средство, которое позволит нам увидеть, как работает система. В реальном мире мы бы не использовали какой-либо генератор, но, вероятно, вместо этого использовали какой-то открытый сервис. Теперь в папке ресурсов создайте новый весенний конфигурационный файл — invoices-context.xml и объявите поддержку сканирования компонентов и планирования задач:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
<? 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 = 'invoices-int-schema.xml' /> < context:component-scan base-package = 'com.vrtoonjava.invoices' /> < context:component-scan base-package = 'com.vrtoonjava.banking' /> < 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
17
|
===========> Sending 10 invoices to the system Amount of $3441 can be automatically processed by system Amount of $17419 can not be automatically processed by system Processing payment Payment{senderAccount=current- local -acc, receiverAccount= test -account 1011000, dollars=3441} Amount of $18442 can not be automatically processed by system Amount of $19572 can not be automatically processed by system Amount of $5471 can be automatically processed by system Amount of $1663 can be automatically processed by system Processing payment Payment{senderAccount=current-iban-acc, receiverAccount= test -iban-2211000, dollars=5471} Amount of $13160 can not be automatically processed by system Amount of $2213 can be automatically processed by system Amount of $1423 can be automatically processed by system Processing payment Payment{senderAccount=current-iban-acc, receiverAccount= test -iban-8051000, dollars=1663} Amount of $1267 can be automatically processed by system Payment failed: org.springframework.integration.MessageHandlingException: com.vrtoonjava.banking.PaymentException: Banking services are offline, try again later! Processing payment Payment{senderAccount=current-iban-acc, receiverAccount= test -iban-6141000, dollars=1423} Processing payment Payment{senderAccount=current- local -acc, receiverAccount= test -account 6761000, dollars=1267} |
Ссылка: Spring Integration — Приложение с нуля, часть 2 от нашего партнера JCG Михала Вртиака в блоге vrtoonjava .