Hiding The Middleware from Your Domain Code With Camel
By James Carr, OCI Software Engineer
March 2010
Introduction
A very important aspect of writing software is trying to write a domain that focuses on the actual domain concepts as much as possible. That is, we want our domain logic to only deal in terms of the domain, rather than dealing in fancy enterprise terms like SOAP, JMS, or REST. By keeping the middleware code hidden from the application code, the application is more flexible and easier to change, and Apache Camel helps achieve that without having to resort to using a full blown Enterprise Service Bus.
Using Camel, I'll demonstrate how you can take two objects that collaborate in the same thread, move them to separate threads, and even separate them altogether behind web services or JMS and continue to work with the same API and not even care about the underlying transport.
The Domain
For the sake of simplicity the domain has purposefully been kept small, limited to just three objects to represent an system that takes an order of some sort (let's call it an OrderSite
) which performs some basic business logic and creates a ShippingOrder
to send to a Warehouse
.
- // src/main/java/com/ociweb/example/domain/OrderSite.java
- public class OrderSite {
- private Warehouse warehouse;
-
- public void process(Order order) {
- ShippingOrder shippingOrder =
- new ShippingOrder().forPurchase(order);
- shippingOrder.shipsOn(calculateShippingDate(shippingOrder));
- warehouse.shipProduct(shippingOrder);
- }
-
- private DateMidnight calculateShippingDate
- (ShippingOrder shippingOrder) {
- return shippingOrder.getQuantity() < 10 ? oneDayFromNow()
- : threeDaysFromNow();
- }
-
- private DateMidnight oneDayFromNow() {
- return new DateTime().plusDays(1).toDateMidnight();
- }
-
- private DateMidnight threeDaysFromNow() {
- return new DateTime().plusDays(3).toDateMidnight();
- }
-
- public void setWarehouse(Warehouse warehouse) {
- this.warehouse = warehouse;
- }
- }
-
- // src/main/java/com/ociweb/example/domain/Warehouse.java
- public interface Warehouse {
- void shipProduct(ShippingOrder order);
- }
- // src/main/java/com/ociweb/example/domain/ShippingOrder.java
- import org.joda.time.DateMidnight;
-
- public class ShippingOrder {
-
- private int quantity;
- private String product;
- private DateMidnight date;
-
- public Integer getQuantity() {
- return quantity;
- }
-
- public String getProduct() {
- return product;
- }
-
- public ShippingOrder forPurchase(Order order) {
- this.quantity= order.getQuantity();
- this.product = order.getProductName();
- return this;
- }
-
- public DateMidnight getExpectedShipDate() {
- return date;
- }
-
- public void shipsOn(DateMidnight date) {
- this.date = date;
- }
- }
- // src/main/java/com/ociweb/example/domain/Order.java
- public class Order {
- private final int quantity;
- private final String productName;
-
- public Order(int quantity, String productName) {
- this.quantity = quantity;
- this.productName = productName;
- }
-
- public int getQuantity() {
- return quantity;
- }
-
- public String getProductName() {
- return productName;
- }
- }
We'll also include an implementation of Warehouse
named SprocketWarehouse
that simply prints the order received and puts the thread to sleep for 1 second to simulate a time consuming process.
- // src/main/java/com/ociweb/example/domain/SprocketWarehouse.java
- public class SprocketWarehouse implements Warehouse {
- private static final String MESSAGE =
- "Recieved Order for %d %s that will ship on %s.%n";
- public void shipProduct(ShippingOrder order) {
- System.out.printf(MESSAGE,
- order.getQuantity(),
- order.getProduct(),
- order.getExpectedShipDate().
- toString("MM/dd/yyyy"));
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {}
- }
- }
These objects will remain largely unchanged throughout the article; the essence is to illustrate keeping your domain code free from any middleware specific transports.
Setting Up Camel
Luckily, Camel comes with a very rich set of configuration options ranging from using Spring, Guice, or just use plain old Java objects to configure itself. But before jumping in, let's first review some of the terminology I'll use:
- Camel Context. If you're familiar with Spring, you can picture the Camel context as being similar to the application context. It's the heart of Camel and is what you'll use to manage rules, routes, and endpoints that you'll register. You can also have multiple Camel context instances within the same VM.
- Endpoint. An endpoint is essentially a true-to-form implementation of
MessageEndpoint
from the book Enterprise Integration Patterns. In Camel, an endpoint represents a destination (e.g., a JMS queue, a file, a web service, or even an irc channel). Endpoints are defined in Camel using Components that work as factories for endpoints. - Route. A Route defines a specialized set of routing rules to route a message within the Camel context. Camel allows users to define routes a number of ways, using a Java DSL, an xml DSL, and even a highly readable Scala DSL.
With all this in mind, let's set up a Spring
based configuration that configures a Camel context to route a message from the OrderSite
to the SprocketFactory
using the direct component, which directly calls the object at the end of the endpoint.
- <!-- src/main/resources/com/ociweb/example/application-context.xml -->
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:camel="http://camel.apache.org/schema/spring"
- xsi:schemaLocation="
- http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
- http://camel.apache.org/schema/spring
- http://camel.apache.org/schema/spring/camel-spring.xsd">
- <camel:camelContext id="context">
- <camel:proxy id="warehouseProxy"
- serviceUrl="warehouse"
- serviceInterface="com.ociweb.example.domain.Warehouse" ></camel:proxy>
- <camel:endpoint id="warehouse" uri="direct:warehouse"></camel:endpoint>
- <camel:route>
- <camel:from ref="warehouse"></camel:from>
- <camel:to uri="bean:warehouseImpl"></camel:to>
- </camel:route>
- </camel:camelContext>
-
-
- <bean id="warehouseImpl"
- class="com.ociweb.example.domain.SprocketWarehouse" ></bean>
-
- <bean id="orderSite"
- class="com.ociweb.example.domain.OrderSite">
- <property name="warehouse" ref="warehouseProxy"></property>
- </bean>
- </beans>
This can look a little complicated with spring.xml, but it gets better. [Line 10] is the camelContext
, which is where we'll store our routes, endpoints, and proxies. [Line 11] is a Camel proxy bean, which uses Spring Remoting under the hood to create a dynamic proxy for the Warehouse
interface that we can inject into OrderSite
to treat it like any other Java object. The serviceUrl
attribute is a reference to the endpoint for this proxy, which in this case we name "warehouse", which references the id of the endpoint we've defined. [Line 14] specifies an endpoint element to specify a uri for the endpoint, set to "direct:warehouse
". This means that any messages received will call a direct invocation on whatever consumers are attached to the endpoint. [Line 15] specifies a route that says from that endpoint we should call whatever is defined by to, which points to the bean definition for warehouse [Line 22]. And in [Line 25] we do the usual chore of injecting the proxy bean on OrderSite
.
One interesting point to discuss is how the bean component works. You might be wondering how it knows which method to call. In this case SprocketWarehouse
contained only one method, so Camel defaulted to calling that method. When there are multiple methods, it will try to call whichever method matches the type. Alternatively, you can also specify which method you want invoked by passing a method parameter to it, for example: bean:sprocketWarehouse?method=shipProduct.
Seeing It In Action
Now to see all this work, let's write a simple class to load up the context, lookup the OrderSite
and submit a few orders:
- import org.springframework.context.support.ClassPathXmlApplicationContext;
-
- import com.ociweb.example.domain.Order;
- import com.ociweb.example.domain.OrderSite;
-
- public class PojoExampleWithSpring {
- public static void main(String... args){
- ClassPathXmlApplicationContext ctx =
- new ClassPathXmlApplicationContext(
- "classpath:com/ociweb/example/pojo-example.xml");
- OrderSite site = (OrderSite) ctx.getBean("orderSite");
-
- site.process(new Order(9, "Sprockets"));
- site.process(new Order(10, "Sprockets"));
- site.process(new Order(13, "Sprockets"));
- site.process(new Order(225, "Sprockets"));
- }
- }
Now if we run this we'll get what we'd expect; each shipping order printed out as they are received, in the order they were sent, with a bit of a lag between each order being printed due to the thread sleeping for one second. Nothing fancy, and at this point you could even argue we overcomplicated having one object simply call another.
Placing The Dependency In Another Thread
We've decided it would be nice if we could multi-thread our order process. Rather than having to wait for each order to be processed sequentially, we instead want each Warehouse
to handle the order in its own thread. Thankfully, this is an easy task to move towards using our existing setup. We just need to modify our camelContext
a bit:
- <camel:camelContext id="context">
- <camel:proxy id="warehouseProxy" serviceUrl="warehouse"
- serviceInterface="com.ociweb.example.domain.Warehouse" ></camel:proxy>
- <camel:endpoint id="warehouse"
- uri="seda:warehouse?concurrentConsumers=4&waitForTaskToComplete=Never"></camel:endpoint>
- <camel:route>
- <camel:from ref="warehouse"></camel:from>
- <camel:to uri="bean:warehouseImpl"></camel:to>
- </camel:route>
- </camel:camelContext>
All that is done here is we change the uri to use the seda component which provides us with better asynchronous invocations as well as concurrency. We can pass options to the component in the form of uri parameters, setting concurrentConsumers to 4 and waitForTaskToComplete to Never, meaning it will call the method and just keep going. Re-running the example should now give us a result of all four warehouses printing their messages simultaneously and out of order as result of them all executing at once.
That's pretty exciting. We just made our app multi-threaded, and we didn't have to write one line of new code or mix a bunch threading logic into our domain. We can use the uri format of the component to configure it as we wish, limiting or expanding the maximum number of concurrent consumers we want executing at once as well as many other options.
For this article, I'm going to stop with different endpoints here because giving examples in each one could be worthy of an entire book! You can take a look at the Components Supported page on the Apache Camel Documentation site if you want to see all the components that Camel supports. Just a few interesting ones are cometd, CXFRS, FTP, and even some as far out there as GMail and IRC. Let's move on to some of the other features Camel supports.
Using The Plain Java DSL
Before we continue, let's re-write our route in plain Java. Conditionals and other concepts can start looking quite nasty as XML and I personally prefer writing them in Java, both for to increase readability and to make it easier to add dynamic functionality to your routing rules. All that needs to be done is create an implementation of RouteBuilder:
- // src/main/java/com/ociweb/example/routes/RouteBuilder.java
- public class WarehouseRoute extends RouteBuilder{
- @Override
- public void configure(){
- from("warehouse").to("bean:warehouseImpl");
- }
- }
Now we update the spring xml to reference the new RouteBuilder we just created:
- <camel:camelContext id="context">
- <camel:proxy id="warehouseProxy" serviceUrl="warehouse"
- serviceInterface="com.ociweb.example.domain.Warehouse" ></camel:proxy>
- <camel:routeBuilder ref="route"></camel:routeBuilder>
- <camel:endpoint id="warehouse"
- uri="seda:warehouse?concurrentConsumers=4&waitForTaskToComplete=Never"></camel:endpoint>
- </camel:camelContext>
-
- <bean id="route" class="com.ociweb.example.routes.WarehouseRoute"></bean>
Notice the route is referenced as just a plain bean that we reference with a routeBuilder
tag. We can now remove the old routing rules, as they now live within the Java class. Now lets add some enhanced functionality.
Implementing a Content Based Router
So far we've made our application multi-threaded, but now we've decided that we want to add a new warehouse that handles bulk orders, where bulk orders have been defined as ShippingOrders
that exceed 200 items. How can we route a message to different endpoints based on the message contents? This is a good problem for which a Content Based Router from the book Enterprise Integration Patterns comes in handy. This type of router is specified to "examine the message content and route the message onto a different channel based on data contained in the message". Camel implements ContentBasedRouter
as a choice which branches by evaluating various predicates.
To illustrate this, let's first create our new Warehouse
implementation to handle bulk orders.
- public class BulkOrderWarehouse implements Warehouse {
- private static final String MESSAGE =
- "Received Bulk Order for %d %s that will ship on %s.%n";
- public void shipProduct(ShippingOrder order) {
- System.out.printf(MESSAGE,
- order.getQuantity(),
- order.getProduct(),
- order.getExpectedShipDate().toString("MM/dd/yyyy"));
-
- for(int i =0; i < 1000000;i++);
- }
- }
In reality, this would probably have a bunch of logic to handle bulk orders and maybe even reside on a different server or maybe even in a different geographical location. But for the example, it's just a copy of the SprocketWarehouse
with a different message. We also modify our Spring xml to register the new bean and name it and the SprocketWarehouse
implementation to distinguish between them.
- public class WarehouseRoute extends RouteBuilder{
- @Override
- public void configure(){
- from("warehouse").
- choice().
- when(quantityIsGreaterThan(200)).
- to("bean:bulkOrderWarehouse").
- otherwise().
- to("bean:sprocketWarehouse");
- }
-
- private Predicate quantityIsGreaterThan(final int bulkQuantity){
- return new Predicate(){
- public boolean matches(Exchange exchange) {
- return
- exchange.getIn().getBody(ShippingOrder.class).getQuantity()
- >= bulkQuantity;
- }
- };
- }
- }
The code is pretty readable — essentially it's making a choice on any messages coming from the warehouse endpoint, and when the quantity is greater than 200 it sends it to bulkOrderWarehouse
, otherwise it will send it to the SprocketWarehouse
. We use a Camel Predicate
object to evaluate the exchange, getting the body as a ShippingOrder.class
and performing the expected predicate logic on it.
If we rerun our example now, we'll see from the messages printed on the screen that all orders greater than or equal to 200 are sent to the bulk warehouse. This can also be done in pure xml in the Spring xml, but as mentioned previously it can get rather messy. In addition to creating a custom predicate, you can also use practically any JSR-233 compatible scripting languages such as Groovy, Javascript, or even PHP to add conditions for routing as well.
References
Hopefully all of this can give you a brief overview of the powerful routing capabilities of Camel and how you can utilize it to create highly decouple components that are unaware of the middleware and trasports you are using. This is really only the tip of the iceberg; I haven't even illustrated how Camel can handle an assortment of industry standard message formats or any of the twenty six different enterprise integration patterns it supports. In the end, it helps minimize the pain of dealing with the low level architecture leaving you free to focus on the more important aspect of your application: the domain.
All of the examples illustrated here are available for viewing yourself here. The examples are built using gradle, which you can install from http://gradle.org. Each example runs by simply typing gradle -q run
from the command line.
- [1] Apache Camel
http://camel.apache.org - [2] Enterprise Integration Patterns
http://www.enterpriseintegrationpatterns.com/toc.html - [3] Examples of Enterprise Integration Patterns in Camel
http://camel.apache.org/enterprise-integration-patterns.html
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.