Implementing Enterprise Integration Patterns using Apache Camel
About Me
1st Career
About Me
2nd Career
About Me
3rd Career?
Overview
- Enterprise Integration & Camel concepts
- Developing Camel applications
- Miscellany
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...
Enterprise Integration with Camel
Enterprise Integration - Before
Fortunately*, the picture's changing...
*Fortunately for software developers and consultants, that is...
Enterprise Integration - After
Enterprise Integration - Soon
Text
https://olimould.files.wordpress.com/2009/01/network-diagram-1.jpg
Enterprise Integration Patterns
Enterprise Integration Patterns
http://camel.apache.org/manual/camel-manual-2.16.2.html#chapter-enterprise-integration-patterns
http://www.enterpriseintegrationpatterns.com/
EIP topics we'll cover using Camel
- Messages
- Channels & Endpoints
- Filters
- Routing
- Translation
- Error, logging, more...
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
What is Camel?
- Modular and extensible message routing framework
- Lightweight core supplemented pluggable architecture
- Easy DSL configuration
- Automatic, but extensible, type conversion
- Built-in components allowing routing from/to almost anything
- Built-in logging, testing and monitoring support
- Plays well with other frameworks, like Spring
- Large and vibrant community
Now for some terminology...
Message
- An interface representing a single message
- Messages have a String messageId
- Headers Map<String, Object>
- Attachments Map<String, Object>
- A body of type Object
Endpoint
- The point where a message is produced or consumed
- Examples
- Camel Processor
- POGO bean
- JMS Queue
- Web service
- File, FTP server, AWS S3
- Email address
- Logger
- Twitter, Facebook, SalesForce, more...
Exchange
- An interface abstracting the exchange of messages
- Exchange ID (useful as correlation id)
- Exceptions during routing stored in Exchange
- Properties are Map<String,Object>
- Exchange Pattern is In-Only or In/Out
URL, URI, URN, IRI
- URL: Uniform Resource Locator
- URN: Uniform Resouce Name
- URI: Uniform Resource Indicator
- A URL or URN
- IRI: Internationalized Resource Identifier
- an i18n of URI
- Camel route configurations use URI Strings
- URL style: file://someDirectory?options
- URN style: file:directoryName?options
Component
- A singleton endpoint factory
-
Camel comes with dozens of different components
- Bean, File, FTP, HTTP, Jetty, JMS, Log, SMTP
- You can create and register custom components
// 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
Route
- A step by step movement of a message from one endpoint to the next
- Often with data conversion in between
- Often with processing between
- To configure routes, we use a DSL (domain specific language)
- Spring (XML)
- Java
- Groovy
- Scala
- Rest
- Annotations
- OSGi Blueprint
Route
// 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
Route
// 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
Route
// 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
CamelContext
- Represents the Camel runtime system
- Similar in concept to the Spring ApplicationContext
- We usually just have one of these in an application
CamelContext
Using the CamelContext
- We create a CamelContext object (Let's call it ctx)
- Add endpoints and maybe components
- Add our routes via ctx.addRoutes(RouteBuilder)
- On startup, we invoke ctx.start()
- On shutdown, we invoke ctx.stop()
Developing Camel Applications
- Project setup
- Creating routes
- Data conversion and processing
- Testing routes and processors
- Fun with components
Project setup
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
Creating routes
- I'll be using the Java and (mostly) Groovy DSLs
- They're easier on the eyes than XML
- If you like XML, the Spring XML DSL is for you
- The easiest way to create routes with a DSL is using a RouteBuilder
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");
}
});
Creating routes
from("timer:sayHello?period=5s")
http://camel.apache.org/timer.html
Demo
Hello World
Creating & testing routes
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();
}
}
Creating routes
- A route is defined with the from method
- From an endpoint, you can route the message to processors, logger, and other endpoints
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");
}
});
Creating routes
- You often will want to define new routes for every step in the workflow
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");
}
});
Why use fine grained routes?
- Testability
- Easier to test
- Maintainability
- Camel route logging makes it easier to spot where things went wrong
- Extensibility
- Easier to find clean insertion points for new routes
- Modularity:
- Routes can be aligned in bounded contexts for dividing monolith into microservices
Downside: App feels more complex with more routes
Demo
Inventory Reporting Workflow
Input & Output
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
Our main app
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();
}
}
The RouteBuilder
// 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
}
}
InventoryProcessor
// 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]));
}
}
StockItem
// 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; }
}
InventoryValuator
// 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);
}
}
InventoryReportGenerator
// 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";
}
...
InventoryReportGenerator
...
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
InventoryReportGenerator
...
// @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();
}
}
What I like
- Well defined responsibilities
- Clear flow in and out
- It demonstrates basic Camel routing
What I hate
- We're doing a lot of work doing low level stuff like parsing CSV file
- StringBuilder for report
- No tests
The Inventory Reporting app...
The worst thing is, no automated tests.
I'd be afraid to change anything without a lot of manual testing!
Camel Testing
- Testing processors and beans should be unit tested
- Testing routing within the app should be system tested
- Testing routing and processing end-to-end including to/from endpoints outside the app should be integration tested
Camel Testing
- I prefer Spock for unit testing
- JUnit or TestNG with mocking works well
- Camel provides excellent testing support for system testing, but it only supports JUnit and TestNG
- I have used Spock for this, but often find myself missing the Camel testing goodies
- For integration testing I've used Cucumber and Spock
- I am not a big fan of Cucumber, but if it's your company standard, have at it
Demo
Adding tests
- I'm going to start transitioning the app over to Groovy.
- ... Because Spock is a Groovy-based testing framework
- ... Because our code gets simpler, often much simpler
- ... Because we'll be able to leverage Camel's excellent Groovy DSL support
- ... Because the semicolon on my keyboard is broken from all the Java code so far
Preparing for 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'
}
Preparing for testing
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
}
}
Route testing in general
// 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()
Route testing in general
@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()
}
Testing our RouteBuilder
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...
Testing
// ... 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 ...
Testing
// ... 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 ...
Testing
// ... 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 ...
Testing
// ... 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 ...
Testing
// ... 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)
}
})
}
}
Type Conversion
- As we move through routes, we often need to convert types
- Sometimes, we do the conversion ourselves
- Often, we can let Camel do it using its built-in converters which allow conversion to/from these types:
- File
- String
- byte[] and ByteBuffer
- InputStream and OutputStream
- Reader and Writer
- Document and Source
Data Formats
- Data formats are a related concept to type conversion
- We often need to marshal and unmarshal from one format to another
- Some common formats
- String
- CSV
- JSON-XML
- JAXB
- See this page for a list of built-in data formats
- http://camel.apache.org/data-format.html
- We'll look at three in detail...
Data Formats
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)
Data Formats
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)
Data Formats
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
}
}
Data Formats
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]))
}
}
Processors & Beans
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 {
...
}
}
Processors & Beans
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");
Processors & Beans
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) { ... }
Camel Registry
- Camel uses a registry for its beans
- By default it's a JNDI implementation
- However, Camel plays very well with Spring
- If Spring is used, Camel will rely on the Spring ApplicationContext as its registry
Other Spring Goodies
- Camel uses Spring Transactions as the default transaction handling in components like JMS and JPA
- Camel works with Spring 2 XML processing with the Xml Configuration
- Camel Spring XML Schema's is defined at Xml Reference
- Camel supports a powerful version of Spring Remoting which can use powerful routing between the client and server side along with using all of the available Components for the transport
- Camel provides powerful Bean Integration with any bean defined in a Spring ApplicationContext
- Camel integrates with various Spring helper classes; such as providing Type Converter support for Spring Resources etc
- Allows Spring to dependency inject Component instances or the CamelContext instance itself and auto-expose Spring beans as components and endpoints.
- Allows you to reuse the Spring Testing framework to simplify your unit and integration testing using Enterprise Integration Patterns and Camel's powerful Mock and Test endpoints
- From Camel 2.15 onwards Camel supports Spring Boot using the camel-spring-boot component.
Miscellany
Error Handling
The common experience of enterprise integration can be summed up in one word:
Failure!
Error Handling
Not this kind of failure...
Error Handling
This kind...
Error Handling
Why is integration so hard?
- We are generally optimists and think about sunny day solutions
- We do an okay job of handling errors in our code (think: try/catch), but rest of world is a black box
- Tight schedules squeeze out error handling stories
- We lose control of messages once they start routing
- We embrace most or all of the 8 fallacies of distributed computing
Error Handling
8 Fallacies of Distributed Computing
https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing
Error Handling
So we must anticipate failure
http://bit.ly/1SMvowA
Error Handling
- Camel treats errors (exceptions) as
- Recoverable
- Irrecoverable
- Irrecoverable examples
- NullPointerException
- SQLException from missing driver
- Recoverable examples
- ConnectionException
- IOException
- Errors assumed to be recoverable unless the fault flag set on the Exchange Message
Error Handling
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
}
}
Error Handling
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
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
Error Handling
Dead Letter Channel
http://camel.apache.org/dead-letter-channel.html
Error Handling
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");
Error Handling
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");
Error Handling
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")
Error Handling
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 Components
- There are dozens of Camel components
- Recall a component is an endpoint factory
- We've seen file and timer
- Let's see a few more...
Camel Components
camel-jetty
from("jetty:http://localhost:8080/myapp/myservice")
.convertTo(String)
.process(wsProcessor);
Camel Components
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 Components
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 Components
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
Logging
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
A few more EIP concepts and implementations...
Message Filter
errorHandler(deadLetterChannel("file:/var/dead_letters"));
from("direct:quotes")
.filter(header("type").isEqualTo("widget"))
.to("direct:widgetsOnly");
http://camel.apache.org/message-filter.html
Content Based Router
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
Message Splitting
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
Message Aggregation
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
Message Aggregation
// 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
Message Aggregation
//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
Message Aggregation
http://camel.apache.org/aggregator2.html
- completionTimeout
- completionInterval
- completionSize
- completionPredicate
- completionFromBatchConsumer
- forceCompletionOnStop
- Using a AggregateController
How will we know when we're done aggregating?
Summary
- Camel Concepts
- Developing Camel Applications
- Miscellany
Questions?
Resources
- Manning Camel in Action
- 2nd Ed in MEAP
- camel.apache.org
- /manual/camel-manual-2.16.2.html
- /discussion-forums.html
- /mailing-lists.html
- www.enterpriseintegrationpatterns.com
- Feel free to reach out to me
Thank you!
- Please complete surveys
- Connect with me
- LinkedIn: jackfrosch
- Twitter: @jackfrosch
- Email: jackfrosch@gmail.com
JDD Group - Implementing Enterprise Integration Patterns using Apache Camel
By Jack Frosch
JDD Group - Implementing Enterprise Integration Patterns using Apache Camel
- 2,082