1st Career
2nd Career
3rd Career?
Caveat: This is a brief introduction to Camel. Camel is very simple to use, but offers so many components and capabilities, I probably couldn't cover them all in three full days of training.
However, you don't need to know everything before doing anything, so let's get started...
*Fortunately for software developers and consultants, that is...
Text
https://olimould.files.wordpress.com/2009/01/network-diagram-1.jpg
http://camel.apache.org/manual/camel-manual-2.16.2.html#chapter-enterprise-integration-patterns
http://www.enterpriseintegrationpatterns.com/
Apache Camel implements many of these out of the box, with customization and extension points enabling us to do any kind of integration we need
Now for some terminology...
// The component used in this route is a File
// There is one File component instantiated
from("file:/orders/inbound") // File creates inbound endpoint
.process(someProcessor)
.to("file:/shipping/outbound"); // File creates outbound endpoint
// Note: inbound and outbound are directories. The file name is in a header
// Spring XML DSL
<bean id="someProcessor" class="com.foo.SomeProcessor" />
<bean id="someBean" class="com.foo.PojoBean" />
<camelContext xmlns="http://camel.apache.org/schema/spring">
<route>
<from uri="someComponentX:abc?options"/>
<process ref="someProcessor"/>
<bean ref="someBean"/>
<to uri="someComponentY:abc?options"/>
</route>
</camelContext>
Configure routes with Spring XML DSL
// Using Java DSL
from("someComponentX:abc?options") // Consume a message here
.process(someProcessor) // Process using Camel Processor
.to("bean:someBean") // Process using bean from registry
.to("someComponentY:abc?options"); // Send it to some endpoint
Configure routes with Java DSL
// Using Groovy DSL which is a lot like Java DSL
from('someComponentX:abc?options') // Consume a message here
.process( { Exchange ex -> ... } ) // Process using Closure
.to("bean:someBean") // Process using bean from registry
.to("someComponentY:abc?${options}") // Send it to some endpoint
Configure routes with Groovy DSL
Using the CamelContext
It's really an Inversion of Control framework where we wire up our message routing, endpoints, and processors in configuration outside our domain classes
I'm using Gradle, but Maven is well supported
...
dependencies {
//It needs to be one of the first dependencies to work
compile 'ch.qos.logback:logback-classic:1.1.6'
compile 'org.codehaus.groovy:groovy-all:2.3.11'
// Camel dependencies
def camelVersion = '2.16.2'
compile "org.apache.camel:camel-core:$camelVersion"
compile "org.apache.camel:camel-groovy:$camelVersion"
testCompile 'junit:junit:4.11'
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
testCompile 'cglib:cglib-nodep:2.2'
}
group 'com.jackfrosch.camel'
version '1.0-SNAPSHOT'
apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'idea'
sourceCompatibility = 1.8 // Camel 2.14 onwards requires JDK 1.7 or better
repositories {
jcenter()
mavenCentral()
}
...
build.gradle
...
sourceSets {
integrationTest {
groovy {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/groovy')
}
resources.srcDir file('src/integration-test/resources')
}
}
configurations {
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
all*.exclude group: "org.slf4j", module: "slf4j-simple"
all*.exclude group: "log4j", module: "log4j"
}
idea {
module {
//add integration test source dirs to IDEA
testSourceDirs += file('src/integration-test/groovy')
scopes.TEST.plus += [ configurations.integrationTestCompile ]
downloadJavadoc = false
downloadSources = false
}
}
// dependencies shown earlier
build.gradle
CamelContext ctx = new DefaultCamelContext();
// Use a timer component to generate message events
ctx.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("timer:sayHello?period=5s")
.log("Hello");
}
});
from("timer:sayHello?period=5s")
http://camel.apache.org/timer.html
package com.jackfrosch.camel;
import org.apache.camel.CamelContext;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
final static Logger logger = LoggerFactory.getLogger(HelloWorld.class);
public static void main(String[] args) throws Exception {
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("timer:sayHello?period=1s")
.log("Hello");
}
});
ctx.start();
Thread.sleep(10000);
ctx.stop();
}
}
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:startWorkflow")
.process(processFirstStep)
.process(processSecondStep)
.process(processThirdStep)
.to("someComponent:finalDestination");
}
});
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:startWorkflow")
.routeId("startWorkflow")
.process(processFirstStep)
.to("direct:secondStep");
from("direct:secondStep")
.routeId("secondStep")
.process(processSecondStep)
.to("direct:thirdStep");
from("direct:thirdStep")
.routeId("thirdStep")
.process(processThirdStep)
.to("someComponent:finalDestination");
}
});
Downside: App feels more complex with more routes
SKU,Qty,Price,Taxable
101,30,20.00,Y
102,40,10.00,N
103,100,0.50,Y
Inventory Report - 04 / 03 / 2016
Summary
----------------------------------------------
Total SKUs in inventory: 3
Total Taxable Value in inventory: 650.00
Total Non-taxable Value in inventory: 400.00
----------------------------------------------
Inventory Details
----------------------------------------------
Item # SKU Qty Price Value Tax?
------ ---------- ----- ------- ------- ----
1 101 30 20.00 600.00 Y
2 102 40 10.00 400.00 N
3 103 100 0.50 50.00 Y
----------------------------------------------
--End Report--
Store123_Inventory_2016-04-01_01-35-45.csv
Store123_Inventory_2016-04-01_01-35-45_Report.txt
package com.jackfrosch.camel.inventory;
import org.apache.camel.CamelContext;
import org.apache.camel.impl.DefaultCamelContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InventoryReportingApp {
final static Logger logger = LoggerFactory.getLogger(InventoryReportingApp.class);
public static void main(String[] args) throws Exception {
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new InventoryReporterRouteBuilder());
ctx.start();
Thread.sleep(20000);
ctx.stop();
}
}
// package and import statements omitted
/**
* These routes are a bit contrived, but illustrate some fundamental Camel
* routing and processing ideas
*/
class InventoryReporterRouteBuilder extends RouteBuilder {
@Override
public void configure() throws Exception {
from("file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive") // consumer
.convertBodyTo(String.class)
.process(new InventoryProcessor())
.to("direct:valueInventory");
from("direct:valueInventory")
.process(new InventoryValuator())
.to("direct:reportInventory");
from("direct:reportInventory")
.process(new InventoryReportGenerator())
.to("file:///tmp/camel-demo/inventory/out"); // producer
}
}
// package and import statements omitted
public class InventoryProcessor implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
String input = (String) in.getBody(); // in.getBody(String.class) & no cast
List<StockItem> items = parseInput(input);
in.setBody(items);
in.setHeader("INVENTORY_COUNT", items.size());
}
private List<StockItem> parseInput(String input) {
List<StockItem> items = new ArrayList<>();
String[] lines = input.split("\n");
for(int i = 1; i < lines.length; i++) {
items.add(createStockItem(lines[i].split(",")));
}
return items;
}
private StockItem createStockItem(String[] fields) {
return new StockItem(fields[0], Integer.valueOf(fields[1]),
new BigDecimal(fields[2]), "Y".equals(fields[3]));
}
}
// package and import statements omitted
public class StockItem {
private final String itemSku;
private final int quantityOnHand;
private final BigDecimal markedPrice;
private final boolean taxable;
private BigDecimal itemValuation;
public StockItem(String itemSku, int quantityOnHand, BigDecimal markedPrice, boolean taxable) {
this.itemSku = itemSku;
this.quantityOnHand = quantityOnHand;
this.markedPrice = markedPrice;
this.taxable = taxable;
this.itemValuation = new BigDecimal(quantityOnHand).multiply(markedPrice);
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StockItem stockItem = (StockItem) o;
return itemSku.equals(stockItem.itemSku);
}
public String getItemSku() { return itemSku; }
public int getQuantityOnHand() { return quantityOnHand; }
public BigDecimal getMarkedPrice() { return markedPrice; }
@Override public int hashCode() { return itemSku.hashCode(); }
public boolean isTaxable() { return taxable; }
public boolean isNotTaxable() { return !taxable; }
public BigDecimal getItemValuation() { return itemValuation; }
}
// package and import statements omitted
public class InventoryValuator implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
@SuppressWarnings("unchecked")
List<StockItem> items = (List<StockItem>) in.getBody();
in.setHeader("INVENTORY_TAXABLE_VALUATION",
calculateInventoryValuation(items, StockItem::isTaxable));
in.setHeader("INVENTORY_NONTAXABLE_VALUATION",
calculateInventoryValuation(items, StockItem::isNotTaxable));
}
// Note: The Predicate here is a Java 8 Predicate functional interface, not a Camel Predicate!
private BigDecimal calculateInventoryValuation(List<StockItem> items,
Predicate<StockItem> selector) {
return items.stream() // we could use a parallelStream() here
.filter(selector)
.map(StockItem::getItemValuation)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// package and import statements omitted
public class InventoryReportGenerator implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
in.setHeader(Exchange.FILE_NAME,
createReportFileName(in.getHeader(Exchange.FILE_NAME, String.class)));
in.setBody(buildReport(in));
}
private String createReportFileName(String inputFileName) {
return inputFileName.substring(0, inputFileName.indexOf('.')) + "_Report.txt";
}
...
...
private String buildReport(Message in) {
StringBuilder sb = new StringBuilder();
sb.append("Inventory Report - ")
.append(LocalDate.now().format(DateTimeFormatter.ofPattern("MM / dd / yyyy")))
.append("\n\n")
.append("Summary")
.append("\n----------------------------------------------\n\n")
.append("Total SKUs in inventory: ").append(in.getHeader("INVENTORY_COUNT"))
.append("\n")
.append("Total Taxable Value in inventory: ")
.append(in.getHeader("INVENTORY_TAXABLE_VALUATION"))
.append("\n")
.append("Total Non-taxable Value in inventory: ")
.append(in.getHeader("INVENTORY_NONTAXABLE_VALUATION"))
.append("\n----------------------------------------------\n\n")
.append("Inventory Details")
.append("\n----------------------------------------------\n")
.append(String.format("%6s %10s %5s %7s %7s %6s",
"Item #", "SKU", "Qty", "Price", "Value", "Tax?"))
.append("\n")
.append(String.format("%6s %10s %5s %7s %7s %6s",
"------", "----------", "-----", "-------", "-------", "----"))
.append("\n");
// ... more
...
// @formatter:off
@SuppressWarnings("unchecked")
List<StockItem> items = (List<StockItem>) in.getBody();
int index = 1;
for (StockItem item : items) {
sb.append(String.format("%6d %10s %5d %7.2f %7.2f %6s",
index, item.getItemSku(), item.getQuantityOnHand(),
item.getMarkedPrice(), item.getItemValuation(),
item.isTaxable() ? "Y" : "N"))
.append("\n");
index++;
}
sb.append("----------------------------------------------\n\n--End Report--");
return sb.toString();
}
}
The worst thing is, no automated tests.
I'd be afraid to change anything without a lot of manual testing!
dependencies {
//It needs to be one of the first dependencies to work
compile 'ch.qos.logback:logback-classic:1.1.6'
compile 'org.codehaus.groovy:groovy-all:2.3.11'
// Camel dependencies
def camelVersion = '2.16.2'
compile "org.apache.camel:camel-core:$camelVersion"
compile "org.apache.camel:camel-groovy:$camelVersion"
testCompile "org.apache.camel:camel-test:$camelVersion"
testCompile 'junit:junit:4.11'
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
testCompile 'cglib:cglib-nodep:2.2'
}
// Route testing is integration testing. We'll use CameltestSupport here
// I'm using Groovy syntax, but Java's is similar - just more verbose
class MyTest extends CamelTestSupport {
// Register our routes with the test CamelContext
@Override
protected RouteBuilder createRouteBuilder() {
new InventoryReporterRouteBuilder()
}
@Test
void "verify Route A will send correct Message to Route B"() {
// Mock out routing to route B so we can evaluate what route A sends
context.getRouteDefinition('routeA').adviceWith(context,
new AdviceWithRouteBuilder() {
void configure() throws Exception {
interceptSendToEndpoint('direct:routeB')
.skipSendToOriginalEndpoint()
.to('mock:routeB')
}
})
// create a Notifier so tell us when the routing happened
NotifyBuilder notifier = new NotifyBuilder(context).fromRoute('routeA')
.wereSentTo('mock:routeB')
.whenCompleted(1)
.create()
@Test
void "verify Route A will send correct Message to Route B"() {
//... we previously mocked next route endpoint and configure a notifier
// now set up expectations for the mock
MockEndpoint mockEp = getMockEndpoint('mock:routeB');
mockEp.expectedHeaderReceived('headerName', headerValue);
mockEp.expectedBodiesReceived("The answer is 42"); // one or more bodies
mockEp.resultMinimumWaitTime = 500;
// now send the inbound message to the route we're testing
// template is a ProducerTemplate for sending messages, usually in tests
template.sendBodyAndHeaders('direct:routeA', theBody,
[headerName : headerValue])
// Now do the assertions. Suppose we expect route A to send
// In Java use JUnit assertMethods. With Groovy we can use power assert
assert notifier.matchesMockWaitTime() // Groovy power assert
assertMockEndpointsSatisfied()
}
Routes and Processors
public class InventoryReporterRouteBuilder extends RouteBuilder {
@Override
public void configure() throws Exception {
getContext().setTracing(true); // Camel Tracer logs routing at INFO level
from("file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive") // consumer
.routeId("inventoryReporterEntry")
.convertBodyTo(String.class)
.process(new InventoryProcessor())
.to("direct:valueInventory");
from("direct:valueInventory")
.routeId("valueInventory") // routeId gives us a handle to the route
.process(new InventoryValuator())
.to("direct:reportInventory");
from("direct:reportInventory")
.routeId("reportInventory")
.process(new InventoryReportGenerator())
.to("file:///tmp/camel-demo/inventory/out"); // producer
}
}
class InventoryRouteBuilderTest extends CamelTestSupport {
private static final String TEST_INPUT_FILE_NAME = 'Store123_Inventory_2016-04-01_01-35-45.csv'
private static final String TEST_OUTPUT_FILE_NAME = 'Store123_Inventory_2016-04-01_01-35-45_Report.txt'
List<StockItem> items
@Before
public void setUp() {
super.setUp()
items = createStockItems()
}
@Test
void "test inventoryReporterEntry route"() {
mockToRoute('inventoryReporterEntry', 'direct:valueInventory', 'mock:valueInventory');
NotifyBuilder notifier = createNotifier('inventoryReporterEntry', 'mock:valueInventory')
MockEndpoint mockEp = getMockEndpoint('mock:valueInventory')
mockEp.expectedHeaderReceived(Exchange.FILE_NAME, TEST_INPUT_FILE_NAME)
mockEp.expectedHeaderReceived('INVENTORY_COUNT', items.size())
mockEp.expectedBodiesReceived([items]) // This looks strange, but I'll explain
mockEp.resultMinimumWaitTime = 500
// This will actually write the file out to the inbound directory
template.sendBodyAndHeader('file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive',
createInput(), Exchange.FILE_NAME, TEST_INPUT_FILE_NAME)
assert notifier.matchesMockWaitTime()
assertMockEndpointsSatisfied()
List skus = mockEp.exchanges[0].in.body.collect { StockItem item -> item.itemSku }
assert skus == ['101', '102', '103']
}
// more...
// ... more
@Test
void "test valueInventory route"() {
mockToRoute('valueInventory', 'direct:reportInventory', 'mock:reportInventory');
NotifyBuilder notifier = createNotifier('valueInventory', 'mock:reportInventory')
MockEndpoint mockEp = getMockEndpoint('mock:reportInventory')
mockEp.expectedHeaderReceived(Exchange.FILE_NAME, TEST_INPUT_FILE_NAME)
mockEp.expectedHeaderReceived('INVENTORY_COUNT', this.items.size())
mockEp.expectedHeaderReceived('INVENTORY_TAXABLE_VALUATION', new BigDecimal("650.00"))
mockEp.expectedHeaderReceived('INVENTORY_NONTAXABLE_VALUATION', new BigDecimal("400.00"))
mockEp.expectedBodiesReceived([this.items])
mockEp.resultMinimumWaitTime = 500
// This will actually write the file out to the inbound directory
template.sendBodyAndHeaders('direct:valueInventory', this.items,
[(Exchange.FILE_NAME) : TEST_INPUT_FILE_NAME,
INVENTORY_COUNT : this.items.size()])
assert notifier.matchesMockWaitTime()
assertMockEndpointsSatisfied()
List skus = mockEp.exchanges[0].in.body.collect { StockItem item -> item.itemSku }
assert skus == ['101', '102', '103']
}
// more ...
// ... more
@Test
void "test reportInventory route"() {
mockToRoute('reportInventory', 'file:/tmp/camel-demo/inventory/out',
'mock:reportDir')
NotifyBuilder notifier = createNotifier('reportInventory', 'mock:reportDir')
MockEndpoint mockEp = getMockEndpoint('mock:reportDir')
mockEp.expectedHeaderReceived(Exchange.FILE_NAME, TEST_OUTPUT_FILE_NAME)
mockEp.expectedBodiesReceived(createExpectedReport())
mockEp.resultMinimumWaitTime = 500
// This will actually write the file out to the inbound directory
template.sendBodyAndHeaders('direct:reportInventory', this.items,
[(Exchange.FILE_NAME) : TEST_INPUT_FILE_NAME,
'INVENTORY_COUNT' : this.items.size(),
'INVENTORY_TAXABLE_VALUATION' :new BigDecimal("650.00"),
'INVENTORY_NONTAXABLE_VALUATION' : new BigDecimal("400.00")])
assert notifier.matchesMockWaitTime()
assertMockEndpointsSatisfied()
}
// more ...
// ... more
private String createInput() {
'''SKU,Qty,Price,Taxable
101,30,20.00,Y
102,40,10.00,N
103,100,0.50,Y'''
}
private List<StockItem> createStockItems() {
[
new StockItem('101', 30, new BigDecimal("20.00"), true),
new StockItem('102', 40, new BigDecimal("10.00"), false),
new StockItem('103', 100, new BigDecimal("0.50"), true)
]
}
// more ...
// ... more
private String createExpectedReport() {
"""Inventory Report - ${InventoryReportGenerator.createReportDate()}
Summary
----------------------------------------------
Total SKUs in inventory: 3
Total Taxable Value in inventory: 650.00
Total Non-taxable Value in inventory: 400.00
----------------------------------------------
Inventory Details
----------------------------------------------
Item # SKU Qty Price Value Tax?
------ ---------- ----- ------- ------- ----
1 101 30 20.00 600.00 Y
2 102 40 10.00 400.00 N
3 103 100 0.50 50.00 Y
----------------------------------------------
--End Report--"""
}
// more ...
// ... more
@Override
protected RouteBuilder createRouteBuilder() {
new InventoryReporterRouteBuilder()
}
private NotifyBuilder createNotifier(String fromRouteId, String toRouteUri) {
new NotifyBuilder(context).fromRoute(fromRouteId)
.wereSentTo(toRouteUri)
.whenCompleted(1)
.create()
}
private RouteDefinition mockToRoute(final String fromRouteId,
final String oldToRouteUri,
final String mockToRouteUri) {
context.getRouteDefinition(fromRouteId).adviceWith(context,
new AdviceWithRouteBuilder() {
void configure() throws Exception {
interceptSendToEndpoint(oldToRouteUri)
.skipSendToOriginalEndpoint()
.to(mockToRouteUri)
}
})
}
}
String
from("file:/var/inbox/data.csv")
.marshal().string("UTF-8")
.to("jms:queue:stage")
from("jms:queue:stage")
.process(orderPreparation)
.to("jms:queue:order")
from("jms:queue:order")
.unmarshal().string("UTF-8")
.process(newOrderProcessor)
JSON
// lets turn Object messages into json - component: camel-xstream
// then send to MQSeries
from("activemq:My.Queue1")
.marshal().json()
.to("mqseries:Another.Queue")
// lets use Jackson - component: camel-jackson
from("activemq:My.Queue2")
.marshal().json(JsonLibrary.Jackson)
.to("mqseries:Another.Queue")
// let's use GSON - component: camel-gson
from("activemq:My.Queue3")
.marshal().json(JsonLibrary.Gson)
.to("mqseries:Another.Queue")
// and back from JSON to POJO
from("direct:backToPOJO")
.unmarshal().json(JsonLibrary.Jackson, MyPojo)
.process(pogoProcessor)
CSV
class InventoryReporterRouteBuilder extends RouteBuilder {
@Override
void configure() throws Exception {
getContext().setTracing(true);
CsvDataFormat csv = new CsvDataFormat()
csv.skipHeaderRecord = true
// file here creates a polling consumer
from("file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive")
.routeId("inventoryReporterEntry")
.unmarshal(csv)
.process(new InventoryProcessor())
.to("direct:valueInventory");
from("direct:valueInventory")
.routeId("valueInventory")
.process(new InventoryValuator())
.to("direct:reportInventory");
from("direct:reportInventory")
.routeId("reportInventory")
.process(new InventoryReportGenerator())
.to("file:///tmp/camel-demo/inventory/out"); // a producer
}
}
CSV
class InventoryProcessor implements Processor {
@Override
void process(Exchange exchange) throws Exception {
Message inMsg = exchange.getIn()
List<List<String>> records = inMsg.body as List<List<String>>
List<StockItem> items = parseInput(records)
inMsg.setBody(items)
inMsg.setHeader("INVENTORY_COUNT", items.size())
}
protected List<StockItem> parseInput(List<List<String>> records) {
records.collect { List<String> fields -> createStockItem(fields) }
}
private StockItem createStockItem(List<String> fields) {
new StockItem(fields[0], Integer.valueOf(fields[1]),
new BigDecimal(fields[2]), "Y".equals(fields[3]))
}
}
All along, I've been using processors
from("direct:valueInventory")
.routeId("valueInventory")
.process(new InventoryValuator())
.to("direct:reportInventory");
class InventoryValuator implements Processor {
@Override
void process(Exchange exchange) throws Exception {
...
}
}
Using the Groovy DSL, closures can be coerced into processors for simple processing
from("direct:enterHere")
.process( { Exchange ex -> ex.in.body = 'The answer is 42' } )
.to("direct:nextStop");
Besides process, you can use simple beans
// One POGO bean, with id = serviceBean, has multiple methods
// If method takes
from("direct:enterHere")
.to("bean:serviceBean?method=filter") // bean uri is one way
.to(serviceBean, "map") // object ref is another way
.beanRef("serviceBean", "reduce") // beanRef is another way
.to("direct:nextStop");
// Camel will supply / convert the type as needed
// Camel will try to bind the body of the Exchange to the first parameter; e.g.
/*
The second argument can be:
org.apache.camel.Exchange
org.apache.camel.Message
org.apache.camel.CamelContext
org.apache.camel.TypeConverter
org.apache.camel.spi.Registry
java.lang.Exception
*/
public String doSomething(String body, Exchange exchange) { ... }
Not this kind of failure...
This kind...
We must anticipate expect failure
http://bit.ly/1SMvowA
Why is integration so hard?
8 Fallacies of Distributed Computing
https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing
class OrderProcessor implements Processor {
OrderDao orderDao
@Override
void process(Exchange exchange) {
String orderId = exchange.in.body
try {
Order order = orderDao.load(orderId)
} catch(DataException e) {
Message msg = exchange.in
msg.setFault(true)
msg.body = "Missing Order for order id = ${orderId}"
msg.exception = e
}
}
void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao
}
}
Camel error handling doesn't handle the edges of the app
From Camel in Action, 1st Ed, Manning
However, components often implement some error handling
Camel provides multiple error handlers
Camel provides decent error handling out of the box
// By default, we get defaultErrorHandler with no retries, but we want retries
// so we need to declare at RouteBuilder level in Java/Groovy DSL
// Can also declare a route level which will take precedence over the RB level
errorHandler(defaultErrorHandler() // defaultErrorHandler doesn't do much
.useOriginalMessage() // processors xform msg; retry w/ original
.maximumRedeliveries(5)
.redeliveryDelay(5000)
.retryAttemptedLogLevel(LoggingLevel.WARN))
Dead Letter Channel
http://camel.apache.org/dead-letter-channel.html
Camel "Dead Letter" handling
// After retries exhausted route to dead queue
errorHandler(deadLetterChannel("jms:queue:dead")
.useOriginalMessage()
.maximumRedeliveries(5)
.redeliverDelay(5000)
Exception Handlers
// Exception handlers can deal with specific exceptions that take precedence
// over the error handler. Here, we retry more times, but at longer intervals
// if an IOException is thrown
onException(IOException)
.maximumRedeliveries(10);
.redeliveryDelay(30000)
errorHandler(defaultErrorHandler()
.useOriginalMessage()
.maximumRedeliveries(5)
.redeliveryDelay(5000)
.retryAttemptedLogLevel(LoggingLevel.WARN))
...
Error handling per route
// Error handlers and exception handlers can be on the routes, too
.onException(ConnectionException)
.maximumRedeliveries(7)
.redeliveryDelay(60000)
errorHandler(defaultErrorHandler()
.useOriginalMessage()
.maximumRedeliveries(5)
.redeliveryDelay(5000)
.retryAttemptedLogLevel(LoggingLevel.WARN))
from("jms:queue:inbox")
.process(decryptProcessor)
.to(validateProcessor)
.to(enrichProcessor)
.to("direct:queue:order")
from("jms:queue:order?concurrentConsumers=10")
.onException(IOException)
.maximumRedeliveries(10)
.redeliveryDelay(30000)
.to("http://com.supplier.com/ws/submitOrder")
camel-jetty
from("jetty:http://localhost:8080/myapp/myservice")
.convertTo(String)
.process(wsProcessor);
<== component name
camel-ftp
from("ftp://username@acme.com/inventory/in?password=foobar&delay=1s&move=../archive")
.convertBodyTo(String.class)
.process(new InventoryProcessor())
.to("direct:valueInventory")
from("direct:valueInventory")
.process(new InventoryValuator())
.to("direct:reportInventory")
from("direct:reportInventory")
.process(new InventoryReportGenerator())
.to("sftp://username@acme.com/inventory/reports?password=foobaz")
camel-mail
from("jms://queue:subscription")
.to("smtp://jack@mymailserver.com?password=secret")
from("pop3://jack@mymailserver.com?password=foobar&consumer.delay=60000")
.to("log:email")
from("imap://jack@mymailserver.com?password=foobaz&consumer.delay=30000")
.to("log:email")
camel-quartz
// fire a message every five minutes starting at 12pm (noon) to 6pm on weekdays
from("quartz://monitors/MonitoringTimer?cron=0+0/5+12-18+?+*+MON-FRI")
.to("direct:monitoringService")
// fire a message on the 2nd Monday every month at 2:15 am
from("quartz://housekeeping/MonthlyHousekeepingTimer?cron=0 15 2 ? 1/1 MON#2 *")
.to("direct:housekeepingService")
/* Cron Expressions
Each field from left to right:
1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (optional field)
*/
http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/tutorial-lesson-06
http://camel.apache.org/quartz.html
class InventoryReporterRouteBuilder extends RouteBuilder {
@Override
void configure() throws Exception {
getContext().setTracing(true) // logs route tracing at INFO level
CsvDataFormat csv = new CsvDataFormat()
csv.skipHeaderRecord = true
from("file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive")
.routeId("inventoryReporterEntry")
.to("log:com.jackfrosch.camel") // Logs the message exchange
.unmarshal(csv)
.log('Hey look! Unmarshalled csv: ${body}") // Simple Expression Language
.process(new InventoryProcessor())
.to("direct:valueInventory");
}
}
http://camel.apache.org/logeip.html
http://camel.apache.org/log.html
Simple EL: http://camel.apache.org/simple.html
errorHandler(deadLetterChannel("file:/var/dead_letters"));
from("direct:quotes")
.filter(header("type").isEqualTo("widget"))
.to("direct:widgetsOnly");
http://camel.apache.org/message-filter.html
errorHandler(deadLetterChannel("file:/var/dead_letters"));
from("direct:a")
.choice()
.when(header("type").isEqualTo("widget"))
.to("direct:Widgets_R_Us")
.when(header("type").isEqualTo("gadget"))
.to("direct:Gadgets_R_Us")
.otherwise()
.log(LoggingLevel.WARN, "Unsupoorted type: ${header.type}");
http://camel.apache.org/content-based-router.html
errorHandler(deadLetterChannel("file:/var/dead_letters"));
from("direct:orderEntry")
.split(body()) // if body is a Collection, iterator or array
.to("direct:lineItemHandler")
from("direct:csvOrderEntry")
.split(body(String.class).tokenize("\n")) // split lines
.process(csvToLineItems)
.to("direct:lineItemHandler")
http://camel.apache.org/splitter.html
http://javarticles.com/2015/07/apache-camel-splitter-using-tokenizer-expression.html
from("direct:start")
// aggregate all exchanges correlated by the id header.
// Aggregate them using the BodyInAggregatingStrategy strategy which
// and after 3 seconds of inactivity them timeout and complete the aggregation
// and send it to mock:aggregated
.aggregate(header("id"), new BodyInAggregatingStrategy())
.completionTimeout(3000)
.to("direct:aggregated");
http://camel.apache.org/aggregator2.html
// simply combines Exchange body values into an List (done the Groovy way)
class ListAggregationStrategy implements AggregationStrategy {
Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
Object newBody = newExchange.in.body
if (oldExchange == null) {
newExchange.in.body = [newBody]
return newExchange
} else {
List list = oldExchange.in.body as List
list += newBody
return oldExchange
}
}
}
http://camel.apache.org/aggregator2.html
//simply combines Exchange String body values using '+' as a delimiter
class StringAggregationStrategy implements AggregationStrategy {
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
if (oldExchange == null) {
return newExchange;
}
String oldBody = oldExchange.getIn().getBody(String.class);
String newBody = newExchange.getIn().getBody(String.class);
oldExchange.getIn().setBody(oldBody + "+" + newBody);
return oldExchange;
}
}
http://camel.apache.org/aggregator2.html
http://camel.apache.org/aggregator2.html
How will we know when we're done aggregating?
@ToString(includeNames = true, includePackage = false,
ignoreNulls = true)
class LineItem {
int itemNo
String productNo
BigDecimal price = 0.00
int qty = 1
BigDecimal taxRate = 0.00
}
@ToString(includeNames = true, includePackage = false, ignoreNulls = true,
excludes = 'lineItems')
class Order {
String orderId // this will be our "correlation id"
String postalCode // drives tax on LineItems
List<LineItem> lineItems = []
void addLineItem(LineItem item) { lineItems << item }
BigDecimal getOrderTotal() {
lineItems.collect { LineItem item -> item.price * item.qty
* (1.00 + item.taxRate) }
.inject(0.00) { sum, value -> sum + value }
}
List<Integer> getLineItemIndexes() { lineItems*.itemNo }
}
class SplitterAggregatorRouteBuilder extends RouteBuilder {
OrderHandler orderHandler = new OrderHandler()
ExecutorService cachedThreadPool = Executors.newCachedThreadPool()
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100) // # of line items
void configure() {
from("direct:orderEntry")
.routeId("orderEntry")
.bean(orderHandler, 'prepareForSplit')
.split(body()) // one thread
// .split(body()).parallelProcessing() // 10 threads
// .split(body()).executorService(cachedThreadPool) // variable threads
// .split(body()).executorService(fixedThreadPool) // thread pool
.to('direct:calculateTax')
from("direct:calculateTax")
.routeId("calculateTax")
.log('Received at calculateTax: ${body}')
.bean(orderHandler, 'calculateTax')
.to("direct:aggregator")
// ...
}
}
class SplitterAggregatorRouteBuilder extends RouteBuilder {
//...
void configure() {
// ...
from("direct:aggregator")
.routeId("aggregator")
.log('Received at aggregator: ${body}')
.aggregate(header('orderId'), new LineItemAggregatorStrategy())
.completionSize(header('lineItemCount'))
.bean(orderHandler, 'rebuildOrder')
.to('direct:finished')
from("direct:finished")
.routeId("finished")
.log('Received at finished: Order: ${body} ...')
.log('LineItem order: ${body.itemNumbers}')
}
}
class LineItemAggregatorStrategy implements AggregationStrategy {
@Override
Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
LineItem item = newExchange.in.body as LineItem
if (oldExchange == null) {
newExchange.in.body = [item]
newExchange
} else {
List<LineItem> list = oldExchange.in.body as List<LineItem>
list << item
return oldExchange
}
}
}
class OrderHandler {
void prepareForSplit(Exchange exchange) {
Message msg = exchange.in
Order order = msg.body as Order
msg.headers.orderId = order.orderId
msg.headers.postalCode = order.postalCode
msg.headers.lineItemCount = order.lineItems.size()
exchange.in.body = order.lineItems
}
// ...
}
class OrderHandler {
// ...
void calculateTax(Exchange exchange) {
String postalCode = exchange.in.headers.postalCode
LineItem item = exchange.in.body as LineItem
BigDecimal taxRate
switch(postalCode) {
case '12345':
taxRate = 0.05
break
case '67890':
taxRate = 0.08
break
default:
taxRate = 0.06
}
item.taxRate = taxRate
Thread.sleep(250) // simulating time to calculate it
}
}
class OrderHandler {
// ...
void rebuildOrder(Exchange exchange) {
Message msg = exchange.in
List<LineItem> items = msg.body as List<LineItem>
items.sort { it.itemNo }
Order order = new Order(orderId: msg.headers.orderId,
postalCode: msg.headers.postalCode,
lineItems: items)
msg.body = order
}
}
That's a lot of information
Start small
Experiment with routes
Experiment with components
In no time, you'll get the hang of it!
Code can be found at: https://github.com/jfrosch/intro_to_camel