Статьи

Spring Integration — Приложение с нуля, часть 2

Это вторая часть руководства, где мы создаем приложение для обработки счетов, используя 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:context = 'http://www.springframework.org/schema/context'
 
    <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 .