Replacing code with XML using Spring Integration
A few weeks ago I had a go at implementing a familiar kind of application using some new tools. The basic structure should be well known to most developers:
- Receive something to process
- Do something with it
- Send the result somewhere
This basic structure pervades the software industry and naturally, there is a vast swathe of reinvented wheels for managing the control flow, often multiple implementations in the same company.
I was building a new application with this structure in Java to consolidate a process that used two similar applications down to just one, reducing complexity along the way. The existing apps implement file system polling for input and FTP and SOAP calls for output. As I was doing exploratory work, I had the opportunity to look at different ways of doing things.
I started by bringing the existing code into our new application, rewriting the main
method and control flow boilerplate code, and reusing the input and output code from previous projects. Once the basic structure was in place, I started looking for opportunities to replace some of the hard-to-test boilerplate code with library usage. Since Spring is used extensively on new projects at the office, we looked into what tools were available in the Spring framework that might meet our needs. That's where we came across Spring Integration.
Using Spring Integration we were able to reduce the main method to 2 lines and completely remove all the boilerplate code that wired up the file system listener to a processing step and then to the output system. Ultimately, we had just a few lines of XML orchestrating the entire process.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/file
http://www.springframework.org/schema/integration/file/spring-integration-file.xsd">
<context:property-placeholder location="classpath*:config.properties"/>
<!--Define channels for message-passing-->
<int:channel id="input" datatype="java.io.File"/>
<int:channel id="output" datatype="com.example.myapp.OutputType"/>
<!--Feed the input from a file system monitor-->
<int-file:inbound-channel-adapter channel="input"
directory="file:${inputDirectory}"
filename-pattern="*${updExtension}"
auto-create-directory="false"
auto-startup="true">
<int:poller fixed-delay="${pollingFrequency}"/>
</int-file:inbound-channel-adapter>
<!--Transform input format to output format - the business logic-->
<int:transformer method="transform" input-channel="input" output-channel="output">
<bean class="com.example.myapp.Input2OutputTypeTransformer"/>
</int:transformer>
<!--Publish output over our own interface-->
<int:outbound-channel-adapter id="out" method="publish">
<bean class="com.example.myapp.WebServicePublisher"/>
</int:outbound-channel-adapter>
</beans>
This Spring XML config defines an application which polls the file system for files with a particular extension (FTP'd in by an upstream process), performs some process with those files, then sends the resulting OutputType over a WebServicePublisher interface.
Since all the wiring is in Spring, we can expect it to be well tested. This allows us to focus on testing our business logic and delete our own boilerplate code, reducing the risk of it being rubbish.
To get the application to run, you just need a very simple main
method:
public static void main(String[] args) {
try {
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"beans.xml"});
context.registerShutdownHook();
} catch (BeansException e) {
LOG.fatal("Beans Exception caught. Exiting...", e);
System.exit(-1);
}
LOG.info("End of main method, other threads continue...");
}
The only gotcha here is that after main()
exits, the file system poller will continue to run and keep the application alive. That being said, this is a rather nice way to make the simple things easy and avoid having to maintain a code to poll the file system and pass the objects between components in the chain.
Not shown here are the Input2OutputTypeTransformer
which accepts a File
and returns an OutputType
, and the WebServicePublisher
for sending the transformer's output to a web service. All the glue for running the input file through the transformer and sending the output to where it needs to go are handled by the framework and a few lines of configuraiton.
What I really like about this approach is that not only do I not need to maintain code for passing messages around, I can also change the method of receiving and sending messages without doing any work on the remaining code. It is flexible and allows me to be avoid unnecessary work.