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.PogoBean" />
<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
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
}
}
...
build.gradle
...
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'
}
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();
// Use a timer component to generate message events
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'
}
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
}
}
// I'll use Groovy syntax here, 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(oneOrMoreBodies);
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()
}
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
from("file:/tmp/camel-demo/inventory/in?delay=1s&move=../archive") // polling consumer
.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"); // producer
}
}
CSV
class InventoryProcessor implements Processor {
@Override
void process(Exchange exchange) throws Exception {
Message inMsg = exchange.getIn()
List<List<String>> records = (List<List<String>>) inMsg.body
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 POGO 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...
Why is integration so hard?
8 Fallacies of Distributed Computing
https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing
So we must anticipate failure
http://bit.ly/1SMvowA
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
DefaultErrorHandler is the default error handler in Camel 2.x. By default, exceptions passed back to caller
Dead Letter Channel which supports attempting to redeliver the message exchange a number of times before sending it to a dead letter endpoint
TransactionErrorHandler This is a transaction-aware error handler extending the default error handler
LoggingErrorHandler for just catching and logging exceptions
NoErrorHandler for no error handling
Camel provides multiple error handlers
Dead Letter Channel
http://camel.apache.org/dead-letter-channel.html
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))
from("jms:queue:inbox")
.process(decryptProcessor)
.to(validateProcessor)
.to(enrichProcessor)
.to("jms:queue:order");
Camel "Dead Letter" handling
// after retries exhausted route to dead queue
errorHandler(deadLetterChannel("jms:queue:dead")
.useOriginalMessage()
.maximumRedeliveries(5)
.redeliverDelay(5000)
from("jms:queue:inbox")
.process(decryptProcessor)
.to(validateProcessor)
.to(enrichProcessor)
.to("jms:queue:order");
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))
from("jms:queue:inbox")
.process(decryptProcessor)
.to(validateProcessor)
.to(enrichProcessor)
.to("direct:queue:order");
from("jms:queue:order?concurrentConsumers=10")
.to("http://com.supplier.com/ws/submitOrder")
Camel provides decent error handling out of the box
// 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);
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?