Implement a customizable ESB with Java

Integrate heterogeneous applications seamlessly with an enterprise service bus

Consider an enterprise where you have heterogeneous applications, possibly delivered by different teams, that need to interact with each other but have the following constraints:

  • Each application is not necessarily built using the same technology and might not talk to the others using its native invocation mechanism e.g., a J2EE application and .Net application.
  • Preferably, each application shouldn't transform its requests into the format understood by the target application. Plus, the enterprise has many applications that use the target application.
  • Service components should use an invocation or request mechanism that is natural to them. For instance, an existing J2EE application may take requests only via Java Message Service (JMS).
  • The enterprise is moving towards an architecture where an application concerns itself only with, one, what it knows and, two, what it should pass as parameters when it wants to obtain the services of another application within the enterprise.

Other constraints might require you to have an infrastructure that enables heterogeneous applications to integrate seamlessly without altering their design. An enterprise service bus (ESB) is one way of realizing such an enterprise integration architecture.

Although each enterprise will likely create its ESB in its own unique way, it is important to keep flexibility in mind when considering the definition of an ESB. There's no fixed approach to building one. The idea is to have a connectivity layer that optimizes interactions between service consumers and service providers, one that can respond to event-, message-, or service-oriented contexts.

This article discusses an approach for building an extensible Java-based ESB that supports the most common ESB functional requirements.

Common ESB requirements

An ESB's common requirements are also its most commonly used features:

  1. Routing: The ESB should provide an efficient and flexible routing mechanism.
  2. Transformation: A service component shouldn't need to know the request format of the target service that it might invoke. Based on the requester and the target, the ESB should be able to apply the appropriate transformation to the request so the target can understand it.
  3. Multiprotocol transport: An ESB implementation that talks only JMS or only Web services is not of much value. It should be extensible enough to support multiple message protocols depending upon enterprise needs.
  4. Security: If needed, the ESB should enforce authentication and authorization for access to different service components.

Figure 1 shows an ESB's main architectural components. It has three broad compartments:

  1. Receiver: An ESB exposes different interfaces for allowing the client applications to send messages to the ESB. For example, a servlet could be receiving the HTTP requests for the ESB. At the same time, you could have an MDB (message-driven bean) listening on a JMS destination where client applications can send messages.
  2. Core: This is the main part of the ESB implementation. It handles routing and transformation, and applies security. Typically, it is composed of an MDB that receives the inbound requests and then, based on the message context, applies the appropriate transformation, routing, and security. Details about routing, transport, transformation, and security information can be specified in a XML document (discussed shortly).
  3. Dispatcher: All the outbound transport handlers come under this part of the ESB. You can plug any arbitrary transport handler (email, fax, FTP, etc.) into the ESB.
Figure 1. Architectural components of ESB. Click on thumbnail to view full-sized image.

All of these ESB parts are glued together by an XML document that lists all the routes on which the ESB operates. The different transport handlers, transformers, and retry policies and their connection to different routes are all wired via this XML document.

ESBConfiguration.xml

The XML listing—ESBConfiguration.xml, which appears below—gives us some idea about an ESB's workings. The main elements of interest in ESBConfiguration.xml are the following:

  1. Beans: This element contains zero or more Bean elements.
  2. Bean: This element basically defines the way we create and configure a Bean class. It has the following attributes:
    • name: Unique name that can be used to refer to this bean.
    • className: Fully qualified name of the bean class.
    Each bean can have zero or more Property elements as children. Each Property element has an attribute name that identifies it and a child element of type Value that holds the property value. These properties are actually the JavaBeans-style members of the class that can configure the bean class.
  3. RetryPolicies: This element contains zero or more RetryPolicy children.
  4. RetryPolicy: This element defines the retry policy for a given route. It has an attribute name that can be used to refer to it. It has two child elements named MaxRetries and RetryInterval.
  5. Route: The EsbConfiguration root element can contain zero or more child elements of this type. It basically represents a route for the ESB. It has the following attributes:
    • name: Unique name that can be used to refer to this route.
    • retryPolicyRef: Reference to the retry policy. It should match the RetryPolicy element's name attribute.
    • transformerRef: Reference to a bean that represents the transformer. It should match the Bean element's name attribute.
    The Route element can have one or more child elements of type TransportHandlerRef. This child basically points to a bean that represents an appropriate transport handler that should be used for this route, and the public method name of that bean that should be invoked to send the message. Optionally, the Route element can have one DeadLetterDestination child that points to another route representing a dead-letter destination.

A sample XML document, EsbConfiguration.xml, appears below:

 <?xml version="1.0" encoding="UTF-8"?>
<EsbConfiguration xmlns="http://www.bss.org/esb/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Route name="creditService" retryPolicyRef="100-sec-retry" transformerRef="creditServiceTransform">
      <TransportHandler beanName="creditJMSTransport"/>
      <DeadLetterDestination routeName="DeadLetter"/>
      <AuthConstraint principals="app-1"/>
   </Route>
   <Route name="taxCalculationService" retryPolicyRef="500-sec-retry" transformerRef="taxCalcServiceTransform">
      <TransportHandler beanName="taxCalcWS"/>
      <DeadLetterDestination routeName="DeadLetter"/>
      <AuthConstraint principals="app-2"/>
   </Route>
   <Route name="RedeliveryRequest" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="redeliveryRequestJMSTransport"/>
      <DeadLetterDestination routeName="DeadLetter"/>
   </Route>
   <Route name="DeadLetter" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="deadLetterJMSTransport"/>
   </Route>
   <Route name="Redelivery" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="redeliveryJMSTransport"/>
      <DeadLetterDestination routeName="DeadLetter"/>
   </Route>
   <Route name="Error" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="errorJMSTransport"/>
   </Route>
   <Beans>
      <!-- Transport handlers for the service components. -->
      <Bean name="creditJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>myCreditQueue</Value>
         </Property>
      </Bean>
      <Bean name="taxCalcWS" className="org.bss.esb.transport.webservice.WebServiceHandler">
         <Property name="WsdlUrl" type="java.lang.String">
            <Value>http://www.tax.com/calc</Value>
         </Property>
      </Bean>
      <!-- Transformer beans for the service components -->
      <Bean name="creditServiceTransform" className="org.bss.esb.transform.XSLTransform">
         <Property name="XslUrl" type="java.lang.String">
            <Value>file:///C:/temp/esb/transform/xsl/credit.xsl</Value>
         </Property>
      </Bean>
      <Bean name="taxCalcServiceTransform" className="org.bss.esb.transform.CustomTransform">
         <Property name="ConfigFileUrl" type="java.lang.String">
            <Value>file:///C:/temp/esb/transform/custom/configManager.properties</Value>
         </Property>
      </Bean>
      <!--  Transport handlers for the system queues -->
      <Bean name="redeliveryJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>Redelivery.Queue</Value>
         </Property>
      </Bean>
      <Bean name="deadLetterJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>System.DL.Queue</Value>
         </Property>
      </Bean>
      <Bean name="errorJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>System.Error.Queue</Value>
         </Property>
      </Bean>
      <Bean name="redeliveryRequestJMSTransport" className="org.bss.esb.transport.jms.EsbRedeliveryHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>Redelivery.Request.Topic</Value>
         </Property>
      </Bean>
   </Beans>
   <!-- Defines the retry policies that can be used by various route definitions. -->
   <RetryPolicies>
      <RetryPolicy name="100-sec-retry">
         <MaxRetries>10</MaxRetries>
         <RetryInterval>100</RetryInterval>
      </RetryPolicy>
      <RetryPolicy name="500-sec-retry">
         <MaxRetries>10</MaxRetries>
         <RetryInterval>500</RetryInterval>
      </RetryPolicy>
   </RetryPolicies>
</EsbConfiguration>

ESB behavior

The ESBConfiguration.xml document dictates our ESB's behavior. The EsbRouter MDB loads this XML from a location specified in its deployment descriptor. The information it contains is then organized into a datastructure depicted in Figure 2 below.

Figure 2. In-memory configuration data structure. Click on thumbnail to view full-sized image.

The EsbRouter uses this information (via EsbConfigManager) to decipher the appropriate route, the transformation, if any, to be applied, the security authorization check, etc. The important point to note is the way the Dependency Injection technique, along with inheritance, has been used to decouple various functions (such as multiprotocol message transport and message transformation) of the ESB, thus allowing it to be highly extensible and customizable.

As the class diagram shows, two important interfaces are in the ESB design: TransformHandler and TransportHandler. They allow you to write a specific transformation and transport implementation for routed messages. These implementation classes can then be wired with the routes via Bean elements in EsbConfiguration. For example, in the sample EsbConfiguration.xml document, the following bean definition specifies the transport handler:

   <Bean name="creditJMSTransport" className="com.foo.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>myQCF</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>myCreditQueue</Value>
         </Property>
      </Bean>

This transport handler can then be referred to in a Route node by inserting a TransportHandler child to it like this:

 <TransportHandler beanName="creditJMSTransport"/>

Note
The approach described in this article uses Java interfaces for defining the transport and transform handlers. Thus, any new handler would have to implement the required interface, which might seem intrusive. You can easily modify the EsbConfigManager to use Dependency Injection for calling any arbitrary method of an implementation class, thus eliminating the need to implement an interface. But since the EsbRouter always passes a javax.jms.Message instance, your handler implementation class must use the type javax.jms.Message anyway.

Now let's take a step-by-step account of the processing as it happens in the ESB upon receiving a message destined for a specific route (service or application). Let's assume, for the sake of simplicity, that the two applications interact via JMS.

  1. As per the deployment descriptor, the initial number of instances of EsbRouter MDBs are created.
  2. The EsbRouter's ejbCreate() method is called, which, in turn, completes the following tasks:
    1. Loads the EnvConfig.xml file from the URL specified in its environment entry named java:comp/env/EnvConfigUrl.
    2. From the EnvConfig.xml, it reads the location URL of the EsbConfiguration.xml and parses it into an XmlBean.
    3. Creates and initializes the EsbConfigManager instance, which involves creating and initializing all the beans configured in EsbConfiguration.xml, and then populating the data structures depicted in Figure 2.
    4. Initializes the EsbRouterMonitorMBean instance. (Details on this will follow shortly. For now, just assume it helps in making the ESB JMX (Java Management Extension)-enabled.)
    5. When a service component needs to send a message, say M, to another one via ESB then it puts the JMS message on the ESB's input queue. This results in the invocation of EsbRouter's onMessage() method. The following steps occur through onMessage():
      1. The destination route name R, specified in a property of the incoming message M, is read. The name of this specific property plus others that will be introduced later are configurable via EnvConfig.xml.
      2. The RouteInfo corresponding to the route name R is looked up from the EsbConfigManager instance.
      3. The requester is checked for whether it's authorized to send the message to the selected route. If it is not, the message is discarded. You can customize this behavior. For example, you can log the message to a special queue or page an administrator.
      4. If a corresponding TransformHandler bean is configured, then it is looked up via the EsbConfigManager instance and its transformMessage() method is invoked by passing the message M as an argument to it.
      5. On each TransportHandler obtained for route R, the EsbRouter invokes the transportMessage() method, passing the input message M as a parameter.
      6. If no entry is found in the EsbConfigManager for the route R, then the TransportHandler for the system dead-letter route is looked up. The message M is then sent to the dead-letter route.
      7. Else, if the given route's handler is found, but the TransportHandler fails to send the message or there's a system error, then message M is queued for redelivery. Queuing for redelivery involves the following steps:
        1. The retry policy is looked up from the EsbConfigManager for the given route.
        2. Based on the retry interval in the retry policy, the next delivery time is calculated.
        3. This time is set as a property MessageRedeliveryTime on the message and it's sent to the redelivery queue.
        4. A separate redelivery request message is sent to the redelivery request topic. Redelivery request is a javax.jms.ObjectMessage that has a Long object representing the next redelivery time calculated above as the payload.

Message redelivery: Scheduling and processing

On receiving a redelivery request message, the RedeliveryRequestProcessor MDB creates a RedeliveryTask instance. RedeliveryTask is initialized with the redelivery time (specified in the redelivery request message as a Long) and is scheduled via the RedeliveryScheduler. The RedeliveryScheduler is a singleton class that delegates the scheduling job to its associated java.util.Timer (see the class diagram). The RedeliveryTask's run() method is called on the scheduled time at which it tries to receive a message from the configured redelivery queue. The receiver it uses has a message selector such that it picks only that message whose MessageRedeliveryTime property matches the scheduled time. This message is then sent to the ESB's input queue.

A redelivered message M destined for a given route R is processed as follows:

  1. The retry policy is looked up from the EsbConfigManager for the given route.
  2. A property holding the past redelivery attempts for this message is read from M. If there's no such property and the maximum retries is zero as per the retry policy, then the message M is sent to the dead-letter route and subsequent steps are omitted.
  3. Else, if the redelivery attempts for M exceeds the maximum retries allowed as per the retry policy, then M is sent to the dead-letter route associated with route R.
  4. Else
    1. The redelivery-attempts count is incremented by one.
    2. The next redelivery time is calculated based on the retry interval in the retry policy.
    3. Both the incremented attempts count and the next redelivery time are set as properties in M.
    4. The message M is sent to the redelivery route. For the redelivery route, the appropriate TransportHandler is looked up, as usual, from the TransportHandlerCache.
    5. A redelivery request message is sent to the redelivery request route. This message contains just the future time (calculated in Step 2 as java.lang.Long) when the message M should be scheduled for redelivery.
  5. If any error has resulted in the redelivery attempt, then message M is sent to the dead-letter route associated with the route R. If the send to dead-letter route also fails for some reason, then M is sent to the error route. If the send to the error route also fails, then the transaction of EsbRouter's onMessage() method is rolled back, which results in the message M being put back on the ESB's input queue.
Note
We use a topic instead of a queue for sending redelivery requests because most J2EE containers do not provide the standard timer service, though they might provide their proprietary APIs to complete similar tasks. The ESB implementation described in this article uses the java.util.Timer instance wrapped inside a singleton to do the scheduling. So how does using a topic help? Assume we use a queue instead of a topic. Now think about a clustered environment to which you have deployed the RedeliveryRequestProcessor MDB. Only one of the managed servers in the cluster will receive the request message from the queue. If that managed server dies after scheduling a RedeliveryTask for execution at some future moment, then we might not be able to deliver it in time, which might cause it to expire. In the case of a topic, the request message is sent to all the RedeliveryRequestProcessor MDB instances in the cluster. So even if the managed server dies after scheduling a task, one of the rest will be able to execute it.

In the actual deployment of an ESB, we would assign a common or separate consumer(s) (possibly MDB) for the dead-letter routes associated with each of the configured routes. Such consumer(s) will pick the messages from the dead-letter routes and notify the appropriate administrators or the applications who may be interested in these messages.

UML diagrams

The following UML diagrams depict the ESB's static and dynamic structure as well as various components:

Figure 3. Component diagram. Click on thumbnail to view full-sized image.
Figure 4. Class diagram for EsbRouter. Click on thumbnail to view full-sized image.
Figure 5. Class diagram for RedeliveryRequestProcessor. Click on thumbnail to view full-sized image.
Figure 6. Sequence diagram for EsbRouter. Click on thumbnail to view full-sized image.
Figure 7. Sequence diagram for RedeliveryRequestProcessor. Click on thumbnail to view full-sized image.

Runtime management

A critical system like an ESB should be manageable and monitorable at runtime. Some of the operational tasks, such as updating or adding new routes, and applying the appropriate transformation, transport handlers, and retry policies, should be doable via a console. To allow such control of this article's ESB, some of its core components have been JMX-enabled, which exposes certain key attributes and operations for performing just these sorts of tasks.

The EsbRouter and RedeliveryRequestProcessor MDBs are JMX-enabled. For example, operations are available for:

  • Dynamically updating the configuration loaded by the EsbConfigManager
  • Capturing the throughput statistics
  • Controlling the redelivery scheduler

You can add more operations if you need to control or monitor extra attributes.

Conclusion

Each organization has its own requirements for an enterprise application integration solution such as an ESB, and each is likely to implement it differently. Key to the success of an ESB implementation is a flexible design for customization and extension that doesn't affect existing parts of the whole system. The approach described in this article gives you a simple and extensible ESB design that caters to an ESB's most common requirements, such as multiprotocol message transport, routing, and transformations. Though the design described in this article doesn't use any Inversion of Control containers, it is possible to implement this design by using such frameworks as Spring.

Balwinder Sodhi works as a senior consultant with a large consulting company in Irvine, California. He has more than seven years of IT experience, ranging from developing and architecting EAI solutions to developing backend systems for financial domain projects using J2EE technologies. His areas of interest include SOA, messaging and Web services. Sodhi holds a B.Tech (honors) degree in electronics and communications engineering from R.E.C. Hamirpur, HP India.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more