Spring Integration in Action
An overview of Spring Integrations message transport conduits, their individual characteristics, and how you can get the most of your application by using the right kind of channel for the job.
One newer member of the Spring family, Spring Integration, provides easy to use implementations of common integration patterns. This article taken from Spring Integration in Action, offers an overview of Spring Integration’s message transport conduits, their individual characteristics, and how you can get the most of your application by using the right kind of channel for the job.
Messages don’t achieve anything sitting all by themselves. In order to do something useful and pass along the information they’re packaging, they need to travel from one component to another and, for this, we need channels, which are well-defined conduits for transporting messages across the system.
To use a letter analogy, the sender creates a letter and hands it off to the mailing system by depositing it into a well-known location, which is the mailbox. From there on, the letter is completely under the control of the mailing system, which delivers it to various waypoints, until it reaches the recipient. The most that the sender can expect is another reply—but, otherwise, who routes the message and who the physical recipient of the letter is (if, for example, the letter is addressed to an organization rather than a person) are things that are not known to it. From a logical standpoint, the channel is very much like a mailbox—a place where certain components, called Producers, deposit messages that are later on processed by other components, called Consumers. This way, Producers and Consumers are decoupled from each other and are only concerned about what kinds of messages they can send and receive, respectively.
One distinctive trait of Spring Integration, which differentiates it among other enterprise integration frameworks, is the emphasis that the channels play in defining the enterprise integration strategy. They’re not just plain information transfer components, but they play an active role in defining the overall application behavior. The business processing takes place in the endpoints, but you just have to alter the channel configuration to completely change the application’s runtime characteristics.
So, you’ll see channels presented from a logical perspective, but you’ll also get an overview of the various channel implementations provided by the framework, their individual characteristics, and how you can get the most of your application by using the right kind of channel for the job.
Using channels to move messages
To connect the producers and consumers configured within the application we use a channel. All channels within Spring Integration implement the MessageChannel interface shown below, which defines standard methods for the sending of messages. Note, however, that it provides no methods to allow the receipt of messages.
The reason for this is that the message channels provided by Spring Integration fall into two distinct categories that primarily differ with regard to the mechanism for handing the message over to the next endpoint.
Do you have any message for me?
PollableChannels as defined by the interface shown below require the receiver or the framework acting on behalf of the receiver to carry out a periodic check to see if messages are available on the channel. This approach has the advantage that the consumer can process messages at a time of their choosing. The approach can also have downsides, creating a tradeoff between longer poll periods, which create latency in the processing of a message, and computation overhead from more frequent polls that find no messages.
I’ll let you know when I’ve got something
The alternative offered by Spring Integration is the SubscribableChannel interface, which is implemented by channels that take responsibility for notifying subscribers when a message is available. In most cases, however, the JOJO consumer code will remain unaware of the capabilities of the channel from which messages are received since the framework will take responsibility for subscribing or polling, as appropriate.
While it is important to understand the implications of whether a channel pushes out messages or is periodically polled, in most cases the framework will take on the concern of connecting a consumer to a channel, alleviating the complications of defining the appropriate consumer types. To put it plainly, your job is to select the appropriate channel type, and the framework will select the appropriate consumer type (polling or event driven).
The right channel for the job
Spring Integration offers a number of different channel implementations and since is an interface you are also free MessageChannel to provide your own implementations. The type of channel selected will have significant implications for your application, including transactional boundaries, latency, and overall throughput. This section will walk you through the factors to consider and a practical scenario of selecting appropriate channels. In the configuration, we will use the namespace but we will also discuss which concrete channel implementation will be instantiated by the framework.
In our flight-booking Internet application, a booking confirmation results in a number of actions. Foremost in the mind of many businesses is the need to get paid, so making sure that we can charge the provided credit card is a high priority. We will also want to update the number of available seats to ensure we don’t overbook the flight. The system will also be required to send a confirmation email with details of the booking and additional information on the check in process. In addition to a website, our Internet booking application exposes a REST interface to allow third-party integration for flight comparison sites and resellers. Since most of the airline’s premium customers come through the airline’s own website, any design should allow us to prioritise bookings originating from there over third-party integration requests in order to ensure that the airline’s direct customers experience a responsive website even in times of high load.
Is choosing a channel all that difficult?
The selection of channels will be based on both functional and nonfunctional requirements, and there are several decision factors that can help us making the right choice. This section provides a brief overview of the technical criteria and the best practices that you should consider when selecting the correct channels.
▪ Do we need to propagate context information between the successive steps of a process? Thread local variables are used to propagate context when needed in several places but where passing via the stack would needlessly increase coupling, for example, transaction context.
|Atomic boundaries||▪ Do we have “all or nothing” scenarios? Classic example: bank
transaction where credit and debit should either succeed or
▪ Typically used to decide transaction boundaries and, because of that, it is a specific case of context sharing. It influences the threading model and therefore limits the available options when choosing a channel type.
|Buffering messages||▪ Do we need to consider variable load? What is immediate and
what can wait?
▪ The ability of systems to withstand high loads is an important performance factor, but load is typically fluctuating, so adopting a thread-per-message processing scenario will require more hardware resources for accommodating the peak load situations. Those resources will stay unused when the load decreases, so this could be expensive and inefficient. Moreover, some of the steps may be very slow, so resources may be blocked for a long amount of time.
▪ Consider what requires an immediate response and what can be delayed and use a buffer to store incoming requests at peak rate and allow the system to process them at its own pace. Consider mixing the types of processing. For example, an online purchase system that will acknowledge the receipt of the request and do some mandatory steps immediately (credit card processing, order number generation) and respond to the client but will do the actual handling of
|Blocking and non- blocking operations||▪ How many messages can we actually buffer? What should we do
when we can’t cope with demand?
▪ If our application is unable to cope with the number of messages being received and no limits have been in place, we may simply exhaust our capacity for storing the message backlog or breach quality of service guarantees in terms of response turnaround.
▪ Recognizing that the system cannot cope with demands is usually a better option than continuing to build up a backlog.
▪ A common approach is to apply a degree of self-limiting to the system. This can be achieved by blocking the acceptance of new messages when the system is approaching its maximum capacity. This can commonly be a maximum number of messages awaiting processing or a measure of requests received per second.
▪ Where the requester has a finite number of threads for issuing requests, blocking those threads for long periods of time may result in timeouts or quality of service breaches. It may be preferable to accept the message and discard it later if system capacity is exceeded, or set a timeout on the blocking operation to avoid indefinite blocking of the requester.
|Consumption model||▪ How many components are interested in receiving a particular
▪ There are two major messaging paradigms: point-to-point or publish-subscribe. In the former, a message is consumed by exactly one recipient connected to the channel (even if there are more of them); in the latter, the message is received by all of them.
▪ If the processing requirements are that the same and a message should be handled by multiple consumers, they can do this concurrently, and a publish/subscribe channel can take care of that. An example is a mash-up application that aggregates results from searching flight bookings. Requests are broadcast simultaneously to all potential providers, which will respond by indicating whether they can offer a booking or not.
▪ Conversely, if the request should be always handled by a single component (for example, processing a payment), a point-to-point strategy is what we are looking for.
Let’s see how these criteria apply to our flight booking sample.
A channel selection example
Using the default channel throughout, we have three channels: one accepting requests and the other two connecting our services.
In Spring Integration, the default channels are SubscribableChannels, and the message transmission is synchronous. The effect of this is simple—we have one thread responsible for invoking the three services sequentially, as shown in figure 1.
Because all operations are executing in a single thread, there is a single transaction encompassing those invocations. If the transaction configuration does not require new transactions to be created for any of the services, the three service invocations will occur within the scope of transaction.
Figure 2 shows what you get when you configure an application using the default channels, which are subscribable and synchronous.
Having all service invocations happening in one thread and encompassed by a single transaction is a mixed blessing: it could be a good thing in certain applications where all three operations must be executed atomically but takes its toll on the scalability and robustness of the application.
But email is slow and our servers are unreliable
The basic configuration is all well and good in the sunny day case when the email server is always up and responsive and the network is 100 percent reliable. Reality is different. Our application needs to work in a real world where the email server is sometimes overloaded and the network sometimes fails. Analysing our actions in terms of what we need to do now and what we can afford to do later is a good way of deciding on what service calls we should block. Billing the credit card and updating the seat availability are clearly things we need to do now in order to be able to respond with confidence that the booking has been made. Sending the confirmation email is not time critical and we don’t want to refuse bookings simply because the mail server is down. Therefore, introducing a queue between the mainline business logic execution and the confirmation email service will allow us to do just that—charge the card, update availability, and send the email confirmation when we can.
Introducing a queue on our emailConfirmationRequests channel will allow the thread passing in the initial message to return as soon as the credit card has been charged and the seat availability has been updated. Changing the Spring Integration configuration to do this is as simple as adding a child <queue/> element to the <channel/>
Let’s recap how the threading model changes by introducing the QueueChannel, as shown in figure 3.
Because now there isn’t a single thread context that encompasses all invocations, the transaction boundaries change as well. Essentially, every operation that is executing on a separate thread executes in a separate transaction, as shown in figure 4.
We have replaced one of the default channels with a buffered QueueChannel and have set up an asynchronous communication model. What we got was some confidence that long-running operations will not block the application because some component is down or just takes a long time to respond. But, now we have another challenge: what if we need to connect one producer with not only one but two (or more) consumers?
Telling everyone who needs to know that a booking occurred
Up until this point, we have been looking at scenarios where a number of services are invoked in sequence with the output of one service becoming the input of the next service in the sequence. This works well when the result of a service invocation only needs to be consumed once; however, it is common that more than one consumer may be interested in receiving certain messages. In our current version of the channel configuration, bookings that have been successfully billed and have been recorded by the seat availability service pass directly into a queue for email confirmation. In reality, this information would be of interest to a number of services within our application and systems within the enterprise such as Customer Relationship Management systems, which track customer purchases to better target promotions, as well as finance systems monitoring the financial health of the enterprise as a whole.
In order to allow the delivery of the same message to more than one consumer, we will introduce a publish/subscribe channel after the availability check. The publish/subscribe channel provides one-to-many semantics rather than one-to-one semantics provided by most channel implementations. This can be particularly useful when we want the flexibility to easily add consumers to the configuration. If the name of the publish/subscribe channel is known, that is all that is required for the configuration of additional consumers, with no changes to the core application configuration.
The publish/subscribe channel does not in itself support queuing, although it does support asynchronous operation. This can be done by providing a task executor, which is then used to deliver messages to each of the subscribers in separate threads. This may, however, still block the main thread sending the message on the channel where the task executor is configured to use the caller thread or block the caller thread where the underlying thread pool is exhausted.
In order to ensure that a backlog in sending email confirmations does not block either the sender thread or the entire thread pool for the task executor, we can connect the new pub/sub channel to the existing email confirmation queue by means of a bridge. The bridge is an EIP pattern that supports the connection of two channels. So, this will allow the pub/sub channel to deliver to the queue and then have the thread return immediately.
Now that we have made it possible to connect one producer with multiple consumers by the means of a publish/subscribe channel, let’s get to the last challenge and emerge victoriously with our drastically improved application: what if “first come, first served” isn’t always right?
Some customers are more equal than others
In order to ensure that the airline’s direct customers have the best possible user experience, we said we wanted to prioritise the processing of their requests to allow us to render the response as quickly as possible. Using a comparator that prioritises direct customers over indirect, we can modify the first channel to be a priority queue. This will cause the framework to instantiate an instance of PriorityChannel, which creates a queue that is able to prioritise the order in which the messages are received. In this case, we are providing an instance of class implementing Comparator<Message<?>>.
What you just saw is an example of applying different types of channels for solving the different requirements with which the application has challenged us. We started with the defaults and, as we worked through the example, we replaced several channel definitions with the ones that are most suitable for each particular situation encountered. What’s most important is that every type of channel has its own justification, and what may be recommendable in one particular use case may not be something you want to use somewhere else. We illustrated the decision process with the criteria that we find the most relevant in every case.