很久很久以前,不知道有21种设计模式,代码Review的时候被Challenge无数次;当我费尽周折做完一个集成项目后,发现原来有更好的企业集成设计模式。也许人生就是经历些曲折,没试过“集成”的苦,又怎能体会设计模式的必要。Apache Camel官方推荐的入门文章:http://architects.dzone.com/articles/apache-camel-integration。
Take any integration project and you have multiple applications talking over multiple transports on multiple platforms. As you can imagine, in large enterprise applications this can get complex very fast. Much of the complexity stems from two issues:
1. dealing with the specifics of applications and transports, and
2. coming up with good solutions to integration problems.
Making your applications speak transports and APIs is relatively easy on its own. I'm sure everyone knows how to send JMS messages to their broker of choice; though it still requires in depth knowledge of the JMS specification, which many developers may not have. On top of that, what happens when you want to route that JMS message to another application? You then have to take care of mapping the JMS message to the application plus handle any new concepts related to the application. Add a dozen other applications into the mix and you've got quite a headache on your hands.
Ignoring the mechanics of how to connect with multiple transports and APIs, we can focus on the high level design of how applications interact. Fortunately, most solutions to enterprise integration problems have been formalized already. Gregor Hohpe and Bobby Woolfe's book, Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions, boils down years of experience from enterprise architects into a set of sixty five Enterprise Integration Patterns (EIPs). This is great but we still have to hand code all parts of these patterns; these are not packaged solutions, only recommendations.
Apache Camel was created with the intention of addressing these two issues. In this article I'll show you how it actually does this.
What is Camel?
Apache Camel is an open source Java framework that focuses on making integration easier and more accessible to developers. It does this by providing:
? concrete implementations of all the widely used EIPs
? connectivity to a great variety of transports and APIs
? easy to use Domain Specific Language (DSL) to wire EIPs and transports together
Figure 1 shows how these three items actually map to Camel concepts. To give you a good understanding of how Camel is organized, we will discuss Components, Endpoints, Processors, and the Domain Specific Language (DSL). There is of course a lot more going on here under the hood but we'll leave that for another discussion.
Figure 1: High level view of Camel's architecture.
Components are the extension point in Camel to add connectivity to other systems. The core of Camel is very small to keep dependencies low, promote embeddability, etc. and as a result contains only 12 essential components. There are over 60 components outside the core. To expose these systems to the rest of Camel, Components provide an Endpoint interface. By using URIs, you can send or receive messages on Endpoints in a uniform way. For instance, to receive messages from a JMS queue aQueue and send them to a file system directory "c:/tmp", you could use URIs like "jms:aQueue" and "file:c:\tmp".
Processors are used to manipulate and mediate messages in between Endpoints. All of the EIPs are defined as Processors or sets of Processors. As of writing, Camel supports 41 patterns from the EIP book, 6 other integration patterns, and many other useful Processors.
To wire Processors and Endpoints together, Camel defines a Java DSL. The term DSL is used a bit loosely here as it usually implies the involvement of a compiler or interpreter that can process keywords specific to a particular domain. In Camel, DSL means a fluent Java API that contains methods named like terms from the EIP book. Its best explained with an example
1.from("jms:aQueue")
2..filter().xpath("/person[@name='Jon']")
3..to("file:c:\tmp");
Here we define a routing rule in a single Java statement that will consume messages from the "jms:aQueue" Endpoint, send them through a Message Filter Processor, which will then send on messages passing the XPath condition to the "file:c:\tmp" endpoint. Messages failing the condition will be dropped.
You can also configure your routes in a XML-based Spring configuration file. This configuration file is a lot more verbose and less auto complete friendly than the Java DSL; many prefer it though because of its direct access to Spring concepts and no requirement for compilation after changes. Here is what the earlier example would look like in Spring:
01.<camelContext xmlns="http://camel.apache.org/schema/spring">
02.<route>
03.<from uri="jms:aQueue"/>
04.<filter>
05.<xpath>/person[@name='Jon']</xpath>
06.<to uri="file:c:\tmp"/>
07.</filter>
08.</route>
09.</camelContext>
These are the concepts that Camel was built upon. Since then many other interesting features have been added. Details of these are left up to the reader to investigate. To get you started, some of these include:
? Pluggable data formats and type converters for easy message transformation between Artix Data Services, CSV, EDI, Flatpack, HL7, JAXB, JSON, XmlBeans, XStream, Zip, Camel-bindy, etc.
? Pluggable languages to create expressions or predicates for use in the DSL. Some of these languages include: EL, JXPath, Mvel, OGNL, BeanShell, JavaScript, Groovy, Python, PHP, Ruby, SQL, XPath, XQuery, etc.
? Support for the integration of beans and POJOs in various places in Camel.
? Excellent support for testing distributed and asynchronous systems using a messaging approach
? and much more...
Example
A motorcycle parts business, Rider Auto Parts, supplies parts to motorcycle manufacturers. Over the years they've changed the way they receive orders several times. Initially, orders were placed by uploading CSV files to an FTP server. The message format was later changed to XML. Currently they provide a web site to submit orders as XML messages over HTTP. All of these messages are converted to an internal POJO format before processing.
Rider Auto Parts states to any new customers to use the web interface to place orders. However, because of existing agreements with customers, they must keep all the old message formats and interfaces up and running.
Solution using EIPs
Rider Auto Parts faces a pretty common problem; over years of operation businesses acquire software baggage in the form of transports/data formats that are popular at the time. Using patterns from the EIP book we can envision the solution as something like Figure 2.
Figure 2: This shows the solution to Rider Auto Parts integration problem using notation from the Enterprise Integration Patterns book.
So we have several patterns in use here.
1. There are two Message Endpoints; one for FTP connectivity and another for HTTP.
2. Messages from these endpoints are fed into the incomingOrderQueue Message Channel
3. The messages are consumed from the incomingOrderQueue and routed by a Content-Based Router to one of two Message Translators. As the EIP name implies, the routing destination depends on the content of the message. In this case we need to route based on whether the content is a CSV or XML file.
4. Both Message Translators convert the message content into a POJO, which is fed into the orderQueue Message Channel.
The whole section that uses a Content-Based Router and several Message Translators is referred to as a Normalizer. This composite pattern has a unique graphic to depict it but was left out here in favor of its sub-patterns to make things clearer.
Implementation using Camel
As mentioned before, Camel has a small core set of components included by default. The rest of the components exist as separate modules. In applications that require many types of connectivity it is useful to figure out what Camel modules to include. Listing 1 shows the dependencies using Apache Maven for the Camel implementation of the Rider Auto Parts example. Of course, you don't need to use Apache Maven for dependencies - it is just the easiest way to rapidly add new dependencies to your applications. The list of dependencies includes support for core Camel, ActiveMQ, JAXB marshaling, CSV marshaling, and HTTP. To make the example easier to try out, I've opted to use the File endpoint instead of the FTP. If we were using the FTP endpoint we would need to add a dependency on the camel-ftp module as well.
Listing 1: Maven dependencies for the Camel implementation
01.<dependencies>
02.<!-- Core Camel support -->
03.<dependency>
04.<groupId>org.apache.camel</groupId>
05.<artifactId>camel-core</artifactId>
06.<version>${camel-version}</version>
07.</dependency>
08.<dependency>
09.<groupId>org.apache.camel</groupId>
10.<artifactId>camel-spring</artifactId>
11.<version>${camel-version}</version>
12.</dependency>
13.
14.<!-- ActiveMQ connectivity for Camel -->
15.<dependency>
16.<groupId>org.apache.activemq</groupId>
17.<artifactId>activemq-camel</artifactId>
18.<version>${activemq-version}</version>
19.</dependency>
20.
21.<!-- Add support for JAXB marshaling -->
22.<dependency>
23.<groupId>org.apache.camel</groupId>
24.<artifactId>camel-jaxb</artifactId>
25.<version>${camel-version}</version>
26.</dependency>
27.
28.<!-- Add support for CSV marshaling -->
29.<dependency>
30.<groupId>org.apache.camel</groupId>
31.<artifactId>camel-csv</artifactId>
32.<version>${camel-version}</version>
33.</dependency>
34.
35.<!-- Add support for HTTP -->
36.<dependency>
37.<groupId>org.apache.camel</groupId>
38.<artifactId>camel-jetty</artifactId>
39.<version>${camel-version}</version>
40.</dependency>
41.
42.<!-- Embedded ActiveMQ broker -->
43.<dependency>
44.<groupId>org.apache.activemq</groupId>
45.<artifactId>activemq-core</artifactId>
46.<version>${activemq-version}</version>
47.</dependency>
48.<dependency>
49.<groupId>org.apache.xbean</groupId>
50.<artifactId>xbean-spring</artifactId>
51.<version>${xbean-spring-version}</version>
52.</dependency>
53.</dependencies>
While it is perfectly legitimate to use Camel as a standalone Java application, it is often useful to embed it in a container. In this case, we will be loading Camel from Spring. The Spring beans XML file is shown in Listing 2. First we start an embedded Apache ActiveMQ broker and connect Camel to it. We also load up some helper beans that we will reference from the DSL. Finally, the camelContext element tells Camel to look for routes in the org.fusesource.camel package. Routes are Java classes that extend the RouteBuilder class in Camel.
Listing 2: Spring XML file that configures an embedded ActiveMQ broker, several beans used in the Camel route, and initializes the Camel Context to search for routes in the org.fusesource.camel package.
view source
print?
01.<beans xmlns="http://www.springframework.org/schema/beans"
02.xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
03.xsi:schemaLocation="
04.http://www.springframework.org/schema/beans
05.http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
06.http://activemq.apache.org/camel/schema/spring
07.http://activemq.apache.org/camel/schema/spring/camel-spring.xsd
08.http://activemq.apache.org/schema/core
09.http://activemq.apache.org/schema/core/activemq-core.xsd">
10.
11.<broker xmlns="http://activemq.apache.org/schema/core" useJmx="false"persistent="false">
12.<transportConnectors>
13.<transportConnector uri="tcp://localhost:61616" />
14.</transportConnectors>
15.</broker>
16.
17.<bean id="jms" class="org.apache.activemq.camel.component.ActiveMQComponent">
18.<property name="brokerURL" value="tcp://localhost:61616"/>
19.</bean>
20.
21.<bean id="normalizer" class="org.fusesource.camel.OrderNormalizer"/>
22.<bean id="orderHelper" class="org.fusesource.camel.OrderHelper"/>
23.
24.<camelContext xmlns="http://activemq.apache.org/camel/schema/spring">
25.<package>org.fusesource.camel</package>
26.</camelContext>
27.
28.</beans>
The real meat of the Camel implementation lies in the OrderRouter class (shown in Listing 3). This class extends RouteBuilder, so it will be automatically picked up and loaded by Camel's runtime.
Looking back at Figure 2, we need to receive orders from an FTP (substituted with File) and HTTP endpoint, formatted as shown in Listing 4. In the DSL we can specify these incoming endpoints with two from elements. Both from elements are connected to a to("jms:incomingOrderQueue") element, which will send the messages to a queue on the ActiveMQ broker.
Listing 3: Route definitions for the example. The routing rules are specified using a fluent API, referred to as Camel's DSL.
view source
print?
01.public class OrderRouter extends RouteBuilder {
02.
03.@Override
04.public void configure() throws Exception {
05.JaxbDataFormat jaxb = new JaxbDataFormat("org.fusesource.camel");
06.
07.// Receive orders from two endpoints
08.from("file:src/data?noop=true").to("jms:incomingOrderQueue");
09.from("jetty:http://localhost:8888/placeorder")
10..inOnly().to("jms:incomingOrderQueue")
11..transform().constant("OK");
12.
13.// Do the normalization
14.from("jms:incomingOrderQueue")
15..convertBodyTo(String.class)
16..choice()
17..when().method("orderHelper", "isXml")
18..unmarshal(jaxb)
19..to("jms:orderQueue")
20..when().method("orderHelper", "isCsv")
21..unmarshal().csv()
22..to("bean:normalizer")
23..to("jms:orderQueue");
24.}
25.
26.}
In the case of the HTTP endpoint, there are a couple of extra things to mention. First off the HTTP client will be expecting a response from the application so we have to handle that. In Camel, we have full control over what the client gets back from the HTTP endpoint. Each response is determined by the last method in our current route definition (each Java statement is a route definition). In our case we use the transform method to set the response to the constant string "OK". Since we handle the response ourselves, we don’t want any response to come from the JMS incomingOrderQueue. To send to this queue in a fire-and-forget fashion we add the inOnly modifier.
It is important to note at this point that when writing Camel DSL in a modern Java IDE, selection of the next processing step is easy because of auto complete. The auto complete feature basically gives you a list of processors (i.e. EIPs) to choose from at any point in your route. Since fluent APIs chain methods together, the only method you need to remember is the from; all other methods are shown via auto complete.
Listing 4: Incoming message formats; XML on top, CSV below.
1.<?xml version="1.0" encoding="UTF-8"?>
2.<order name="motor" amount="1"/>
3.
4."name", "amount"
5."brake pad", "2"
The next section of DSL in Listing 3 specifies the Normalizer, complete with Content-Based Router and two Message Translators. First we specify that we want to consume messages from the incomingOrderQueue on the ActiveMQ broker. The content based routing of the messages is done with the choice and when methods. In our case, we want to send CSV messages to one Message Translator and XML messages to another. To check what type of message we have we will be using a simple Java bean shown in Listing 5. Of course, this is demonstration code only; for production cases you would want to add more thorough checking of content types.
Listing 5: Java bean that contains helper methods to be used in the DSL.
01.public class OrderHelper {
02.public boolean isCsv(String body) {
03.return !body.contains("<?xml");
04.}
05.
06.public boolean isXml(String body) {
07.return body.contains("<?xml");
08.}
09.}
If the message has XML content, we use the JAXB data format to unmarshal the XML payload into an Order object. As shown in Listing 6, the Order object has JAXB annotations to describe the mapping to XML. You of course don't need to use JAXB here; it just makes things very easy as you don't have to do any nasty XML parsing by hand.
Listing 6: The Order domain class with JAXB annotations for easy mapping to and from XML.
01.@XmlRootElement
02.@XmlAccessorType(XmlAccessType.FIELD)
03.public class Order implements Serializable {
04.@XmlAttribute
05.private String name;
06.@XmlAttribute
07.private int amount;
08.
09.public Order() {
10.}
11.
12.public Order(String name, int amount) {
13.this.name = name;
14.this.amount = amount;
15.}
16.
17.@Override
18.public String toString() {
19.return "Order[" + name + " , " + amount + "]";
20.}
21.}
For the transformation from CSV to Order object, we don't have a nice JAXB analogue. We do have support in Camel for unmarhsaling CSV content into a List though. We use this in combination with a custom bean to do the complete transformation. The OrderNormalizer bean shown in Listing 7 takes the List of Lists created by the CSV unmarshaler and creates a new Order object from it.
Listing 7: Java bean that takes the CSV data and creates a new Order domain object from it.
1.public class OrderNormalizer {
2.public Order fromCsvToOrder(List<List<String>> body) {
3.List<String> orderHeaders = body.get(0);
4.List<String> orderValues = body.get(1);
5.return new Order(orderValues.get(0), Integer.parseInt(orderValues.get(1)));
6.}
7.}
At this point, successfully normalized messages are sent to the orderQueue for processing by some other application at the Rider Auto Parts business.
Conclusion
In this article I've shown two common problems that an integration developer may face: dealing with the specifics of applications and transports, and coming up with good solutions to integration problems. The Apache Camel project provides a nice answer to both of these problems. As the example has shown, solving integration problems with Camel is straight forward and results in relatively concise code. In my opinion it is the closest thing to integration nirvana that we have today.
Links