This is the second part of the tutorial where we are creating an invoices processing application using Spring Integration. In case you missed it, be sure to look at the first part. Previously we’ve defined functional requirements for the system, created gateway, splitter, filter and router component. Let’s continue with creating a transformer.
5. Transforming invoices to the payments
We’ve successfully filtered out “too expensive” invoices from the system now (they might need manual inspection or so). The important thing is that we can now take an invoice and generate payment from it. First, let’s add Payment class to the banking
package:
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(); }}
Because we will have two ways how to create a payment (from local and foreign invoices), let’s define a common contract (interface) for creating payments. Put interface PaymentCreator to the banking package:
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;}
Technically, this is a simple parametrized Factory. Note that it throws PaymentException. We’ll get to the exception handling later, but here’s the code for the simple PaymentException:
package com.vrtoonjava.banking;public class PaymentException extends Exception { public PaymentException(String message) { super(message); }}
Now we’re good to add two implementations to the invoices package. First, let’s create LocalPaymentCreator class:
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;@Componentpublic 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()); }}
Another creator will be ForeignPaymentCreator with rather straightforward implementation:
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;@Componentpublic 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()); }}
Interesting part about creators is @Transformer annotation. It’s a similar concept as we’ve used with @Filter annotation – only this time we’re telling to Spring Integration that it should use this method for payload transforming logic. Either way we will use foreign or local transformer, so new message will end in bankingChannel channel. Let’s define these new transformers in our schema file:
<int:transformer input-channel='localTransactions' output-channel='bankingChannel' ref='localPaymentCreator' /><int:transformer input-channel='foreignTransactions' output-channel='bankingChannel' ref='foreignPaymentCreator' /><int:channel > <int:queue capacity='1000' /></int:channel>
6. Passing payments to the banking service (Service Activator)
Payments are ready and messages containing them are waiting in the bankingChannel. The last step of the flow is to use Service Activator component. The way it works is simple – when a new message appears in a channel, Spring Integration invokes logic specified in a Service Activator component. So when a new payment appears in the bankingChannel, we want to pass it to the banking service.
In order to do that we first need to see a contract for the banking service. So put interface BankingService to the banking package (in the real world this would probably reside in some external module):
package com.vrtoonjava.banking;/** * Contract for communication with bank. */public interface BankingService { void pay(Payment payment) throws PaymentException;}
Now we will need an actual implementation of the BankingService. Again, it’s highly unlikely that implementation would reside in our project (it would probably be remotely exposed service), but let’s at least create some mock implementation for the tutorial purposes. Add MockBankingService class to the banking package:
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); }}
Mock implementation creates on some random occasions (~10%) a failure. Of course for the better decoupling we’re not going to use it directly, we will create dependency from our custom component on a contract (interface) instead. Let’s add PaymentProcessor class to the invoices package now:
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. */@Componentpublic class PaymentProcessor { @Autowired BankingService bankingService; @ServiceActivator public void processPayment(Payment payment) throws PaymentException { bankingService.pay(payment); }}
Again – note the @ServiceActivator annotation. That means that Spring Integration should invoke corresponding method when service activator component comes to the game. To use the service activator we need to add it to the integration schema:
<int:service-activator input-channel='bankingChannel' ref='paymentProcessor'> <int:poller fixed-rate='500' error-channel='failedPaymentsChannel' /></int:service-activator><int:channel />
Note that we’re defining fixed-rate attribute which means that activator will be invoked every half second (if there is some message present in the bankingChannel). We’re also defining error-channel attribute, but we’ll get there just in moment.
Error handling
One of the biggest challenges of messaging systems is to properly identify and handle error situations. Spring Integration provides a technique called “error channels”, where we can (obviously) send error messages from the system. Error channel is just another channel and we can take proper action when an error message appears in this channel. In the real world applications we would probably go for some retry logic or professional reporting, in our sample tutorial we will just print out the cause of the error. In the previous component (Service Activator) we’ve specified error-channel property to refer to the failedPaymentsChannel. When message arrives to this channel we will invoke another Service Activator and print out the error. Here’s the implementation of the FailedPaymentHandler Service Activator:
package com.vrtoonjava.invoices;import org.springframework.integration.annotation.ServiceActivator;import org.springframework.stereotype.Component;@Componentpublic 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 }}
And let’s hook it to the integration schema as usual:
<int:service-activator input-channel='failedPaymentsChannel' ref='failedPaymentHandler' />
Running the whole thing
We’ll create a job now that will (at fixed rate) send new invoices to the system. It is only a standard Spring bean that utilizes Spring’s @Scheduled annotation. So let’s add a new class – InvoicesJob to the project:
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. */@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; }}
Job invokes (every 4 seconds) InvoicesGenerator and forwards invoices to the Gateway (first component we read about). To make it work we also need InvoicesGenerator class:
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; }}
This is only a simple mock facility that’ll allow us to see the system working. In the real world we wouldn’t use any generator but probably some exposed service instead. Now under resources folder create a new spring config file – invoices-context.xml and declare component scanning and task scheduling support:
<?xml version='1.0' encoding='UTF-8'?><beans xmlns = 'http://www.springframework.org/schema/beans' xmlns:xsi = 'http://www.w3.org/2001/XMLSchema-instance' xmlns:task = 'http://www.springframework.org/schema/task' xmlns:context = 'http://www.springframework.org/schema/context' 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 pool-size='10' /> <task:scheduler pool-size='10' /> <task:annotation-driven executor='executor' scheduler='scheduler' /></beans>
To see the whole thing running we need one more last piece – standard Java main application where we will create Spring’s ApplicationContext.
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'); }}
Simply run mvn clean install from command line and launch the main method in InvoicesApplication class. You should be able to see similar output:
===========> Sending 10 invoices to the systemAmount of $3441 can be automatically processed by systemAmount of $17419 can not be automatically processed by systemProcessing payment Payment{senderAccount=current-local-acc, receiverAccount=test-account 1011000, dollars=3441}Amount of $18442 can not be automatically processed by systemAmount of $19572 can not be automatically processed by systemAmount of $5471 can be automatically processed by systemAmount of $1663 can be automatically processed by systemProcessing payment Payment{senderAccount=current-iban-acc, receiverAccount=test-iban-2211000, dollars=5471}Amount of $13160 can not be automatically processed by systemAmount of $2213 can be automatically processed by systemAmount of $1423 can be automatically processed by systemProcessing payment Payment{senderAccount=current-iban-acc, receiverAccount=test-iban-8051000, dollars=1663}Amount of $1267 can be automatically processed by systemPayment 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}
Reference: Spring Integration – Application from scratch, Part 2 from our JCG partner Michal Vrtiak at the vrtoonjava blog.
Source : feedproxy[dot]google[dot]com
No comments:
Post a Comment