A week or so ago, I wrote about how to use Spring Integration to create a new generation of File Sucker. The File Sucker from that article was not much more than a POJO that was wired in Spring to react to messages placed on a specific channel by a file source adapter.
The file source adapter is just one of many channel adapters possible with Spring Integration. As of 1.0-M1, Spring Integration comes with several ready-to-use adapters:
- File source and target adapters
- JMS source and target adapters
- Byte stream source and target adapters
- Character stream source and target adapters
- Application event source and target adapters
The list of channel adapters from 1.0-M! is impressive--but it keeps growing as Spring Integration moves closer to 1.0. Scheduled for 1.0-M2 and 1.0-M3:
- RMI source and target adapters (1.0-M2)
- HttpInvoker source and target adapters (1.0-M2)
- E-mail target adapter (1.0-M2)
- Spring-WS source and target adapters (1.0-M3)
- Spring MVC source adapter (1.0-M3)
Suffice it to say that by the time Spring Integration hits 1.0, there'll probably be an adapter to suit most integration needs.
Today I'm going to show off a few more of Spring Integration's adapters and also show you how to create a custom adapter, just in case your needs exceed what comes out of the box. Let's start by seeing file source adapter's mirror image: the file target adapter.
Sending messages to files
The central idea of integration is moving data from one place to another. Source adapters move information from some external source into an application, while target adapters move information out of a system to some target location (presumably to be picked up by some other application). We've already seen how a file source adapter can suck a file into an application. Now let's see how to spit messages out into files using file target adapters.
Adding a file target adapter to the Spring application context isn't much different than adding a file source adapter. Here's the file sucker application context, this time with a few new elements:
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/integration" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-1.0.xsd"> <message-bus/> <channel id="fileInputChannel" /> <channel id="fileOutputChannel" /> <annotation-driven/> <context:component-scan base-package="com.habuma.si.example" /> <file-source directory="/Users/wallsc/sucker" poll-period="1000" channel="fileInputChannel"/> <file-target directory="/Users/wallsc/spitter" channel="fileOutputChannel" /> </beans:beans>
I've added a new channel named "fileOutputChannel" for messages to travel on as they make their way to the file source target. As for the file source target itself, it is declared with the <file-target>
element. The directory
attribute specifies where the files will be written and the channel
attribute indicates which channel that messages will be written from.
So, how do messages get into "fileOutputChannel"? Good question. For the answer, let's have another look at FileSucker
:
package com.habuma.si.example; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.integration.annotation.Handler; import org.springframework.integration.annotation.MessageEndpoint; @MessageEndpoint(input = "fileInputChannel", defaultOutput = "fileOutputChannel") public class FileSucker { private static final Logger LOGGER = Logger.getLogger(FileSucker.class); @Handler public String suckAFile(String fileContents) { LOGGER.debug(fileContents); return StringUtils.reverse(fileContents); } }
A few things have changed since last time. There is a new attribute on the @MessageEndpoint
annotation. defaultOutput
specifies the default channel that messages will be sent on. As for the messages themselves, they are returned from the suckAFile()
method, which has been altered to return a String
. Specifically, suckAFile()
will return the contents of the file that it was given, using StringUtils
(from Jakarta Commons Lang) to reverse the contents (just to make it more interesting).
With these changes, FileSucker
is effectively also a file spitter. The value returned from suckAFile
goes into "fileOutputChannel", from which it is picked up by the <file-target>
and written to a file. File gets sucked in...file gets spit out.
One interesting thing to note at this point is that despite its name, FileSucker
doesn't even realize that it's sucking and spitting files. It only knows that it is receiving a String
from one channel and sending a String
on another channel. How data arrives in "fileInputChannel" and how it leaves "fileOutputChannel" is none of FileSucker
's concern. For that matter, if you disregard the annotations, FileSucker
doesn't even know that it's being used with for integration purposes. It's simply a POJO that takes a String
, reverses the String
and then returns the reversed String
. Put simply, FileSucker
is decoupled from file reading and writing activities--and decoupling, as I think we can all agree, is a good thing.
Once you have a grasp on the basics of file adapters, working with other flavors of adapters should be straightforward. Let's see how to configure a set of JMS adapters to replace the file adapters in our example.
Using JMS adapters
Along with File Transfer, another common integration style is Messaging. In the Java world, JMS is the API of choice for transferring data in a loosely-coupled manner between applications. Spring Integration supports JMS messaging through JMS channel adapters.
Like most channel adapters supplied by Spring Integration, the JMS channel adapters comes as a matched pair of source and target adapters. These are configured in XML using the <jms-source>
and <jms-target>
elements. For example, here is some Spring XML configuration that configures one of each of the JMS channel adapters:
<channel id="inAndOutChannel" /> <jms-source channel="inAndOutChannel" connection-factory="connectionFactory" destination="inQueue"/> <jms-target channel="inAndOutChannel" connection-factory="connectionFactory" destination="outTopic" /> <beans:bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <beans:property name="brokerURL" value="tcp://localhost:61616" /> </beans:bean> <beans:bean id="inQueue" class="org.apache.activemq.command.ActiveMQQueue"> <beans:constructor-arg index="0" value="sample.queue"/> </beans:bean> <beans:bean id="outTopic" class="org.apache.activemq.command.ActiveMQTopic"> <beans:constructor-arg index="0" value="sample.topic"/> </beans:bean>
This is a fairly simple-minded configuration of JMS channel adapters where the <jms-source>
adapter receives messages on a message queue and the <jms-target>
adapter republishes them on a topic. The messages travel between each adapter via a channel named "inAndOutChannel".
There's also some additional plumbing required by both <jms-source>
and <jms-target>
in order to communicate with the message broker. In this example, I'm using ActiveMQ as the message broker, so I've configured an ActiveMQConnectionFactory
to point to an ActiveMQ instance running on localhost, port 61616 (the default). Naturally, this assumes that ActiveMQ is running and listening on port 61616. I've also declared two destinations: one ActiveMQQueue
and one ActiveMQTopic
. These destinations are wired into <jms-source>
and <jms-target>
, respectively, for listening for and publishing messages.
This example illustrates the basic use of <jms-source>
and <jms-target>
, albeit in an oversimplified way. In a real-world scenario, you're more likely to have an endpoint respond to the messages published by <jms-source>
or perhaps a different endpoint to push messages into the channel that is published by <jms-target>
. In fact, there's nothing stating that you must have both JMS channel adapters in a single integration scenario. You may only need a <jms-source>
to pull data into your application or a <jms-target>
to publish messages from your application...but not necessarily both. And, for that matter, there's no reason you couldn't retrieve messages from JMS and write other messages to a file using <file-target>
.
Creating custom adapters
Although Spring Integration comes packed with several useful channel adapters, your integration needs may require a custom adapter. Fortunately, Spring Integration makes it easy to register POJOs as custom adapters. For example, consider the following DateSourceAdapter
package com.habuma.si.example; import java.util.Date; public class DateSourceAdapter { public Date whatTimeIsItNow() { return new Date(); } }
There's nothing very special about DateSourceAdapter
. In fact, it is a POJO in every possible sense of the acronym. However, as you'll soon see, this is a fully functional source adapter. In the Spring configuration, we'll declare DateSourceAdapter
to be a source adapter:
<channel id="dateInputChannel" /> <beans:bean id="dateAdapter" class="com.habuma.si.example.DateSourceAdapter" /> <source-adapter channel="dateInputChannel" period="1000" ref="dateAdapter" method="whatTimeIsItNow" />
Focus your attention on the <source-adapter>
element, which is where the magic happens. This element declares that the bean whose ID is "dateAdapter" should have its whatTimeIsItNow()
method invoked every second. Whatever value is returned fromwhatTimeIsItNow
should be published into the channel named "dateInputChannel" (from which someone else will pull the value and process it).
An interesting thing to note about the whatTimeIsItNow()
method is that it returns a Date
object. This illustrates that messages do not have to be String
s. Messages can be composed of more complex objects, as long as the receiving endpoint or adapter knows how to deal with those types.
For example, here's DateFormatter
, an endpoint that receives Date
objects from "dateInputChannel", formats them, and then returns them to the message bus as String
s:
package com.habuma.si.example; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.integration.annotation.Handler; import org.springframework.integration.annotation.MessageEndpoint; @MessageEndpoint(input = "dateInputChannel", defaultOutput = "formattedDateChannel") public class DateFormatter { private DateFormat dateFormat; public DateFormatter(String format) { dateFormat = new SimpleDateFormat(format); } @Handler public String slurpDate(Date date) { return dateFormat.format(date); } }
Again, DateFormatter
is oblivious as to the source of the dates that it will format and is equally unaware of how the formatted dates will be used. It simply receives Date
s as they arrive on "dateInputChannel" and drops the formatted dates off with "formattedDateChannel".
Although DateFormatter
doesn't really care what will be done with the String
s that it drops into "formattedDateChannel", it might be interesting for us to see how they'll be dealt with by creating a custom target adapter to process messages on "formattedDateChannel". Here's a simple adapter that simply prints a String
to stdout
:
package com.habuma.si.example; public class StdoutTargetAdapter { public void printToStdout(String string) { System.out.println(string); } }
As with DateSourceAdapter
, StdoutTargetAdapter
is a very simple POJO. In fact, aside from its name, there's no clue that it will be used as a channel adapter. But when the following XML is placed in the Spring configuration, StdoutTargetAdapter
comes to life, printing out dates that arrive in the "formattedDateChannel":
<channel id="formattedDateChannel" /> <beans:bean id="stdoutAdapter" class="com.habuma.si.example.StdoutTargetAdapter" /> <target-adapter channel="formattedDateChannel" ref="stdoutAdapter" method="printToStdout" />
The <target-adapter>
element is the key to StdoutTargetAdapter
's power. It receives messages as they arrive in "formattedDateChannel" and dispatches them to the printToStdout()
method of the "stdoutAdapter" bean (which happens to be StdoutTargetAdapter
).
These custom adapters are rather trivial, but they should give you an idea of how to write your own adapters to solve whatever integration problems you may have.
That's all for today's exploration of Spring Integration's channel adapters. Next time, perhaps I'll dig into some of Spring Integration more by looking at tricks like routing and splitting messages. See you then!