Wednesday, June 25, 2008

Trying out Spring Integration

When building our Virtual Enterprise (1-1.5 years ago), we ended up using Mule ESB (version 1.3.3 at the time) to handle protocol mediation. The intent at the time was to focus on getting our hands into various SOA product suites in more of a real world situation. Therefore, we really didn't want to spend the time to write the code required to write to or read from a JMS queue, or call a web service, or write code to communicate over whatever protocol our enterprise requires. We also wanted the ability to change the protocol over which any endpoint within the Virtual Enterprise communicated. This would allow us to mimic numerous scenarios without having to make coding changes. Mule ESB gave us these capabilities mainly through the configuration of connectors and routers on POJOs.

With the introduction of Spring Integration (currently milestone 4), I have decided to take a look back at one of the endpoints in our Virtual Enterprise and convert it from Mule to Spring Integration. The main motivation for this is to get my hands into Spring Integration and start to get a feel for the capabilities it offers. The endpoint I chose was the Emergency Alert System (EAS). This is a spring web application that runs an embedded instance of Mule for the protocol mediation mentioned above. This application communicates via message queues. When an alert is created, it sent to a JMS queue that kicks off a business process which subsequently notifies other external systems. During various points in the business process, information flows back into the EAS via other message queues.

In order to accomplish this with Mule, the following mule configuration file was required:


<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mule-configuration PUBLIC "-//SymphonySoft //DTD mule-configuration XML V1.0//EN"
"http://www.symphonysoft.com/dtds/mule/mule-spring-configuration.dtd">

<mule-configuration id="SOA_LAB_EAS" version="1.0">

<mule-environment-properties serverUrl="tcp://localhost:60505"/>

<!-- Use this connector if using ActiveMQ as the JMS provider -->
<connector name="jmsConnector" className="org.mule.providers.jms.JmsConnector">
<properties>
<property name="specification" value="1.1"/>
<property name="connectionFactoryJndiName" value="ConnectionFactory"/>
<property name="jndiInitialFactory" value="org.apache.activemq.jndi.ActiveMQInitialContextFactory"/>
<map name="connectionFactoryProperties">
<property name="brokerURL" value="tcp://localhost:61616"/>
<property name="brokerXmlConfig" value="classpath:/activemq-config.xml"/>
</map>
</properties>
</connector>

<transformers>
<transformer name="JmsMessageToString" className="org.mule.providers.jms.transformers.JMSMessageToObject" returnClass="java.lang.String"/>
<transformer name="StringToJmsMessage" className="org.mule.providers.jms.transformers.ObjectToJMSMessage" returnClass="javax.jms.TextMessage"/>
</transformers>

<model name="EAS">
<mule-descriptor name="EASCallReceiver" implementation="conferenceCallReceiver">
<inbound-router>
<endpoint address="jms://alert.inbound.call.queue" transformers="JmsMessageToString"/>
</inbound-router>
</mule-descriptor>
<mule-descriptor name="EASNotificationReceiver" implementation="notificationInfoReceiver">
<inbound-router>
<endpoint address="jms://alert.inbound.notification.queue" transformers="JmsMessageToString"/>
</inbound-router>
</mule-descriptor>
</model>

</mule-configuration>

As you can see, this configuration defines the appropriate JMS Connection information for connecting to ActiveMQ, the transformers necessary to convert to and from JMS Text Messages, and the Mule endpoints (the implementations refer to Spring beans defined in my Spring config file) with the appropriate routers defined for the inbound and outbound JMS Queues. This configuration isn't too bad. In order to embed Mule within my Spring application, I had to add 16 JARS to my classpath. Then, when the web application would start up, the embedded Mule server would start up as well, sharing the spring context, and everything connected nicely. In order to send messages to the outbound queue, I used the MuleClient class to get access to the endpoints, and let the Mule server handle the rest. The code was as simple as this:

MuleClient muleClient = new MuleClient();
muleClient.sendNoReceive(outboundEndpoint, messageBody, null);

where outboundEndpoint is a String value injected into my class which references the URI for the JMS queue (i.e. jms://alert.outbound.queue). As for the inbound messages, they were handled by Mule based on the inbound routers configured on the Mule Endpoints. Once a message was delivered to a queue, Mule transformed it to a string (based on the defined transformers) and routed it to my POJO. Overall, we were quite pleased with this scenario.

With the introduction of Spring Integration, we were interested in seeing if we could use this framework in the same way we were using Mule. So I went about the task of converting the embedded Mule configuration to Spring Integration. In the end, it really wasn't too difficult.

The first step in my conversion process was to send the outbound message to the alert.outbound.queue (an ActiveMQ queue). I only had to add the following JARS to my classpath: org.springframework.integration-1.0.0.M4.jar and org.springframework.integration-adapter-1.0.0.M4.jar.

In order to use spring integration to route my messages, I first defined a message bus. This is accomplished by declaring the following component in my spring configuration file:

<integration:message-bus/>
Next, I needed to define a channel, and connect that channel to a target endpoint. This is accomplished with the following configuration:

<integration:channel id="outboundAlertChannel"/>
<integration:target-endpoint input-channel="outboundAlertChannel" target="jmsTarget"/>

Since we are targeting a JMS endpoint, the target attribute refers to a JMS target bean configured like this:

<integration:jms-target id="jmsTarget" connection-factory="jmsFactory" destination-name="alert.outbound.queue"/>
This uses an ActiveMQ connection factory and defines the queue destination on which to place the outbound message.

Then in my POJO, I added setters to inject the MessageChannel (represented by the channel element in my configuration) so I could send the message to the channel.

outboundAlertChannel.send(new StringMessage(messageBody));

Then the Spring Integration framework would take over from there to route the message to the alert.outbound.queue.

Next, I had to configure the inbound messages and route them to a particular method on another POJO. This was pretty straightforward as well, and was accomplished entirely through configuration. I needed to define a channel, a JMS Source, and a handler endpoint. In this case, I wanted to follow a message driven approach rather than a polling approach, so I used a JMS Gateway. Unfortunately, there is a bug when using namespace support for JMS Gateway configuration (that is already fixed, but not in milestone 4), so I had to configure it using a bean declaration. The configuration for receiving the message from the queue and routing to my POJO can be seen below:


<integration:channel id="inboundAlertNotificationChannel"/>

<bean id="jmsGateWay" class="org.springframework.integration.adapter.jms.JmsGateway" init-method="start"
destroy-method="destroy">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="requestChannel" ref="inboundAlertNotificationChannel"/>
<property name="destinationName" value="alert.inbound.notification.queue"/>
<property name="expectReply" value="false"/>
</bean>

<integration:handler-endpoint input-channel="inboundAlertNotificationChannel" handler="notificationInfoReceiver" method="receiveNotificationInfo"/>

The jmsGateway connects the queue to the channel and the handler-endpoint connects the channel to my POJO, specifying the method to call on the POJO. The message is converted by the framework from a JMS Text message to a String. That was it, no coding changes required for this modification.

I found using Spring Integration (in this simple example) very logical, straightforward, and lightweight. I guess these seem to be typical attributes that come to mind when using Spring.

12 comments:

Ali Taimur said...

Hello,

Is it possible that you could post your complete spring-integration configuration? I am also trying to learn Spring-Integration by converting a mule application into a Spring-Integration application and you complete config would help alot.

Thanks

Steve Smith said...

Sure. Here is my complete spring integration configuration file (this does not include my application context file)

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:integration="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration-1.0.xsd">

<!-- define the message bus -->
<integration:message-bus/>

<!-- define the channel for the outbound comms -->
<integration:channel id="outboundAlertChannel"/>

<!-- JMS target for outbound alerts -->
<bean id="jmsFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>tcp://localhost:61616</value>
</property>
</bean>


<integration:jms-target id="jmsTarget" connection-factory="jmsFactory" destination-name="alert.outbound.queue"/>

<integration:target-endpoint input-channel="outboundAlertChannel" target="jmsTarget"/>


<!-- JMS Source for receiving inbound JMS messages -->
<integration:channel id="inboundAlertNotificationChannel"/>

<!-- There is a bug using the jms-gateway tag, so use the bean def instead until the next release -->
<!--<integration:jms-gateway request-channel="inboundAlertNotificationChannel" connection-factory="jmsFactory" destination-name="alert.inbound.notification.queue" expect-reply="false"/>-->
<bean id="jmsGateWay" class="org.springframework.integration.adapter.jms.JmsGateway" init-method="start"
destroy-method="destroy">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="requestChannel" ref="inboundAlertNotificationChannel"/>
<property name="destinationName" value="alert.inbound.notification.queue"/>
<property name="expectReply" value="false"/>
</bean>

<integration:handler-endpoint input-channel="inboundAlertNotificationChannel" handler="notificationInfoReceiver" method="receiveNotificationInfo"/>

</beans>

Ali Taimur said...

Thank You, this helps a lot

charliem said...

In order to simplify your life, have a look to Apache Camel project and you will understand immediately the advantages of this framework in combination to spring integration or alone to build ESB solutions. The framework is Spring oriented, based on Enterprise Integration Patterns and propose a DSL language to build the routes between the endpoints. The routes can also be created using Spring XML config files.

Bill lin said...

I build my project base on spring-integration-M6 and activemq5.0.0,
but it's very strange that the jmsGateway doesn't listen the msq continually.
The endpoint will only get the first msg in queue when the application is starting up but not geting all the msg.
According to the description of gatway --"Spring Integration's message-driven JmsGateway is more appropriate since it delegates to a MessageListener container" ,we know it's different with polling.
Why?

My configuration:

<beans:bean class="org.springframework.integration.adapter.jms.JmsGateway">
<beans:property name="connectionFactory" ref="connectionFactory"/>
<beans:property name="requestChannel" ref="receiver" />
<beans:property name="destinationName" value="TB.BR.queue"/>
<beans:property name="expectReply" value="true"/>
</beans:bean>

<beans:bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory ">
<beans:property name="brokerURL" value="failover:(tcp://localhost:61616)?initialReconnectDelay=100" />
<beans:property name="copyMessageOnSend" value="false" />
<beans:property name="objectMessageSerializationDefered" value="true" />
</beans:bean>

Bill lin said...

I have get the key.
Modify the property of expect-reply from true to false and the listener can work normally.
But I can not find out the detail description of the expect-reply property. Could u give me some advice?

Steve Smith said...

Bill,

The expect-reply property is telling the gateway to send the message and wait for a reply (up to a configured timeout). Setting this to false, allows the gateway to send the message and complete.

Thanks,
Steve

Bill lin said...

Steve,
Thanks a lot!
But how to send reply and how set the gateway to stop when initializing(init-method=stop is invalidated)? I must do more studying.

Ross Mason said...

Hi Steve,
Mule 1.3.3 is a pretty old version, things are very different in Mule 2. The config is a lot more intuitive and much less verbose, see the follow example based on your config:

http://pastebin.com/f4a063fcd

(Blogger wouldn't let me post XML)

Cheers,
Ross

raja said...

Hi Steve,
when I use your configuration file, I am getting following error from xml file
"cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element 'integration:jms-target'"
can you help me out
Advance Thanks....

Steve Smith said...

Raja,

I believe the example is not using the current release of Spring Integration (actually I think it was using an RC). I'm sure there have been changes to the schema since I published this example.

A Kumar said...

this is a very helpful post.