A Standalone Service Tier with Spring
As web developers, we all-too-often have a propensity to try to wrap everything that we do into a web bundle of some sort. Indeed, we will even go so far as to take measures to ensure that the architecture of a service-tiered application will fit into the mold of a deployable web application. In some cases, it makes sense to do this, while in other cases you may garner the benefits of a lower memory footprint, or simplified project structure, by merely building the components of your service tier as standalone applications.
An excellent use-case for building a standalone application is one in which we need some service-layer functionality that is triggered by an event from another application in the enterprise. A typical solution to this might be to develop the service as a web application and expose REST endpoints to which the caller can interface. The architectural question that you need to ask yourself at this point is whether or not your integration point needs to be REST. If the answer is no, then you can probably simplify this code by developing the app in a message-driven fashion.
Perhaps your web stack is already pretty saavy, and you’ve adapted several use-cases to employ RabbitMQ as a queuing and messaging broker between your applications and services. Indeed, no application stack could be considered truly web scale without having RabbitMQ in there somewhere, right? Maybe you’ve even taken the bold architectural measure of defining data domains within your enterprise, and building business components as modular, deployable applications atop those verticals. If you have, then you’re already half-winning the battle to a really good infrastructure. Unfortunately, for simplicity and as a stop-gap measure, you’ve probably exposed those business-oriented functions through REST endpoints, and rely on so-called Web Service calls from consumers to make things happen, when in reality your services just need to get a message and go off to do some work.
For this post, we’ll talk about developing those service-tiered applications in a manner such that the integration point for your service layer is message-driven. To be more specific, the concept that will be covered in this post will be related to how applications can produce enterprise-wide events to trigger some functionality at your service tier. This functionality can be anything from indexing data for search, to de-normalizing an object graph in a reporting database, to generating reports in real time, to something as simple as sending an email. The one thing that all of these functions have in common is that they need to be triggered by some event within your stack.
The pain of building an enterprise eventing system has been so greatly reduced by the wonderful work done on the Spring Integration project. In fact, so much of building message-driven applications on top of Spring Integration breaks down to be merely configuration, which leaves you free to focus on writing domain and business logic. This rids you of the burden of having to build frameworks that adapt concepts just to get to some message data.
Spring Integration also frees you from having to worry about developing bridges and adapters for the various brokers that may be responsible for delivering your messages. In this, your business logic never has to change if you decide that, for example, you want to switch from an AMQP broker to a JMS one. In fact, Spring Integration gives you all of the niceties of an old-fashioned Enterprise Service Bus, without the collateral misuse of buzz-words by those in management.
To kick off our contrived scenario, we’ll begin by building a service class that is designed to consume some data and do some processing with it. What that data is and what processing is being done with it is wholly at your discretion; for the purposes of this example, we’ll simply be printing a receipt of the message to standard-out.
The goal in all this is not to interrogate the process by which we write business code, but rather to demonstrate how to tie a Plain-Old-Java/Groovy-Object to a messaging endpoint, simply through configuration. To that end, let’s examine the Spring configuration wherein we configure the bridge from a RabbitMQ queue to our service class.
The Spring team has (successfully) gone to extensive measures to simplify an otherwise incredibly complex process of wiring up all the parts necessary to interact with RabbitMQ. The spring-rabbit project provides namespace support for the rabbit schema, as demonstrated in the above configuration. The biggest part of this configuration is the rabbit:admin tag, which looks for exchanges, queues, and bindings, and declares those relationships to the RabbitMQ server on behalf of the developer. Beyond that, it is just a matter of us describing those components to the Spring context to have them at our disposal.
An important thing to note with respect to AMQP is that all communication occurs over queues. If you’re not coming from a strong background in messaging, the concept of exchanges and queues can be difficult to grasp. Queues are bound to exchanges, which allows developers to send a message to an exchange, and have RabbitMQ figure out to which queue the message will be delivered. This leaves producers (apps sending the messages) ignorant to what queue should be used for an exchange, and allows consumers (apps receiving messages) to configure routing rules, which can inspect a message to determine its destination queue. Given this, a more practical way to look at this is, producers write a message to an exchange, consumers listen for messages on a queue, RabbitMQ orchestrates the hand-off from exchange to queue (the binding).
For the purposes of demonstrating a message-driven enterprise eventing system, the exchange that our queue will be bound to will be a fanout exchange. Fanout exchanges deliver a copy of each message to all queues bound to it. This means that we can have many service application listening to queues on this exchange, and when a message comes in, they can go off and do whatever processing they need to do with it. A fanout exchange can be thought of as a broadcast exchange, and no feedback is provided to the application that sent the message. Simply put, set it and forget it.
In order to get the messages from the queue to our service class, we need to do some bridging to get there. Normally, this would involve writing a MessageListenerContainer that called out to our class when the message was received, but this process is very programmatic and tightly couples our service class to the queue. In the example configuration above, the amqp namespace is provided and supported as part of the spring-integration-amqp extension. Using this namespace, we can define an inbound-channel-adapter, which leverages the already-defined RabbitMQ connection factory to listen on a configured queue. This channel-adapter is the Spring Integration bridge between the RabbitMQ queue and an in-memory messaging channel, which is defined as msgs-input.
It’s important to note when looking at the example configuration that the base namespace is defined with the Spring Integration schema, so we can define a channel without needing to declare a prefixed namespace.
The inbound-channel-adapter is then configured with the defined msgs-input channel, so that messages are received, converted, and handed off without having to write a single line of code. We can then fall back to proper Spring Integration configuration and attach a service-activator to the input channel. The service-activator in this case is a reference to our service class. The @ServiceActivator annotation over the process method on the MessageProcessor class tells Spring Integration that this is the method that is to be used to handle the data when it comes in.
Now that the service class is configured to receive messages from RabbitMQ, we need only develop a light-weight runnable class that fires up the Spring context.
This class will use the @ImportResource annotation to bring in our XML configuration, while itself is also defined as a Spring Java configuration class. Defining the runnable class as a @Configuration class is a technique that I picked up that allows you to remain very flexible when swapping between Spring XML configuration and Spring Java configuration. Usually, I prefer the Spring configuration to be Java configuration, but when using Spring Integration, there is so much more support with the XML configuration, that it is not worth the effort to rework all of that stuff into a Java configuration. Perhaps this will get better in the future, but for now, this is my recommended approach.
The ScheduledProducer inner class is simply a demonstration of sending messages to the exchange. Running this class will produce a series of Sending: $num, Receiving: $num messages in your console, from it and the MessageProducer class accordingly.
The next step is to build this as a standalone application, which means that we should produce a package that is deployable to our server environment. For this, we can leverage the Gradle application plugin, which will place all of the application’s dependencies in the build/install/si-mda/lib directory, along with runnable scripts in build/install/si-mda/bin.
To achieve this, the application plugin gives us an installApp task that we can run to see this process work. When we’re ready to build a deployable, simply running the distZip Gradle task will produce a zip file with the contents of the bin and lib directories that we need to run our application. Now we can take this package and deploy it off to our server environment.
The runtime environment for this project takes less than 100mb of memory to run. Compare that to a minimum of 1GB memory required to fire up even the most trivial of Grails applications, and you’ve got yourself a big win at your service tier. Additionally, building, packaging, deployment, and startup times all take a fraction of the time when compared to their web-based counterparts.
Ultimately, when designing your application infrastructure, take the time to consider your service implementations, and ask the real question of whether those services needs to be a web applications, or whether you can do them better, faster, and lighter-weight as standalone apps.