apache camel
这是本教程的第二部分,我们将使用Apache Camel创建发票处理应用程序。 如果您错过了它,一定要看一下第一部分 。 以前,我们已经定义了系统的功能要求,创建了网关,分离器,过滤器和基于内容的路由器组件。 让我们继续创建一个转换器。
5.将发票转换为付款
现在,我们已经成功地从系统中过滤掉了“过于昂贵”的发票(它们可能需要人工检查等)。 重要的是,我们现在可以收取发票并从中产生付款。 首先,让我们将Payment
类添加到banking
包中:
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
包:
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
的代码:
package com.vrtoonjava.banking;
public class PaymentException extends Exception {
public PaymentException(String message) {
super(message);
}
}
现在,我们可以将两个实现添加到invoices
包中了。 首先,让我们创建LocalPaymentCreator
类:
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
,它具有相当简单的实现:
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 bean,而Apache Camel提供了一种将它们连接到路由的非常好的方法。 我们将在Camel的Java DSL上使用transform()
方法创建两个转换器。 我们将正确的转换器插入seda:foreignInvoicesChannel
seda:localInvoicesChannel
和seda:foreignInvoicesChannel
seda:localInvoicesChannel
,并使它们将结果转发到seda:bankingChannel
。 将以下代码添加到您的configure
方法中:
from("seda:foreignInvoicesChannel")
.transform().method("foreignPaymentCreator", "createPayment")
.to("seda:bankingChannel");
from("seda:localInvoicesChannel")
.transform().method("localPaymentCreator", "createPayment")
.to("seda:bankingChannel");
6.将付款转至银行服务(服务激活器)
付款已经准备就绪,包含付款的消息正在seda:bankingChannel
中等待。 该流程的最后一步是使用Service Activator组件。 它的工作方式很简单-当频道中出现新消息时,Apache Camel会调用Service Activator组件中指定的逻辑。 换句话说,我们正在将外部服务连接到我们现有的消息传递基础结构。
为此,我们首先需要查看银行服务合同。 因此,将BankingService
接口BankingService
到banking
程序包中(在现实世界中,它可能驻留在某些外部模块中):
package com.vrtoonjava.banking;
/**
* Contract for communication with bank.
*/
public interface BankingService {
void pay(Payment payment) throws PaymentException;
}
现在,我们将需要BankingService
的实际实现。 同样,实现不太可能驻留在我们的项目中(它可能是远程公开的服务),但是至少出于教程目的,让我们创建一些模拟实现。 将MockBankingService
类添加到banking
包:
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
包中:
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提供了一种简单的方法,当消息到达特定端点时,如何在任意bean上调用方法( EIP将其描述为服务激活器),方法是在Camel的Java DSL上使用bean()
方法:
from("seda:bankingChannel")
.bean(PaymentProcessor.class, "processPayment");
错误处理
消息传递系统的最大挑战之一是正确识别和处理错误情况。 EAI描述了很多方法,我们将使用Camel的Dead Letter Channel EIP的实现。 死信通道只是另一个通道,当此通道中出现错误消息时,我们可以采取适当的措施。 在现实世界的应用程序中,我们可能会寻求一些重试逻辑或专业报告,在我们的示例教程中,我们将仅打印出错误原因。 让我们修改先前定义的Service Activator并插入errorHandler()
组件。 当PaymentProcessor
引发异常时,此errorHandler会将引起错误的原始消息转发到Dead Letter Channel:
from("seda:bankingChannel")
.errorHandler(deadLetterChannel("log:failedPayments"))
.bean(PaymentProcessor.class, "processPayment");
最后,这是最终的完整路线:
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的@Scheduled
注释的标准Spring bean。 因此,我们向项目添加一个新类– InvoicesJob
:
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;
}
}
Job会(每4秒)调用InvoicesGenerator
并将发票转发到Gateway(我们了解的第一个组件)。 为了使其工作,我们还需要InvoicesGenerator
类:
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
文件夹下创建一个新的spring配置文件– invoices-context.xml
并声明组件扫描和任务计划支持:
<?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 = "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主应用程序,我们将在其中创建Spring的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");
}
}
只需从命令行运行mvn clean install
并在InvoicesApplication类中启动main
方法。 您应该能够看到类似的输出:
===========> 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}
apache camel