Skip to main content

Do You Want to Get This Message?

Posted by manning_pubs on October 19, 2012 at 11:52 AM PDT



Do You Want to Get This Message?

by Mark Fisher, Jonas Partner, Marius Bogoevici, and Iwein Fuld, authors of Spring Integration in Action

Spring Integration allows you to selectively process messages and define alternative routes within the system. In this article, based on chapter 6 of Spring Integration in Action, you'll see how you can limit the scope of what your components will handle by using filters that allow only certain messages to pass through to be processed.

Save 43% on Spring Integration in Action and other Spring Framework titles. Click here

The main role of an endpoint is to consume messages from a channel and process them by invoking the associated MessageHandler. This means the service is invoked for every message that arrives on the inbound channel.

It's possible that a given consumer isn't interested in all the messages they receive. For example, in a publish-subscribe scenario, different consumers may have different interests in the incoming messages even if they're all potential recipients.

To enable the selective consumption of messages, you can use a special type of message handler, a message filter. As you can see in figure 1, the filter is a message handler that evaluates messages without transforming them and publishes back to an output channel only the ones that satisfy a given rule.

Figure 1 A message filter evaluates incoming messages. Only As are published to the next channel, whereas Bs and Cs are discarded.

Of course, the decision of whether a certain component can process certain types of messages can be made by the component itself. For example, the plain old Java object (POJO) implementation of a service activator can decide whether messages are valid. Embedding this decision into the component works well if the decision is based on a broad general principle that's applicable everywhere that component is used. Filters are useful wherever the filtering condition is based on an application-specific requirement that's not inherently linked to the Java implementation of the business service.

Filtering out messages

The application can receive cancellation requests through a gateway. The requests are forwarded to a cancellation processing service, and further to a notification service, which sends out confirmation emails. Cancellations for different types of reservations may need to be processed differently because the conditions may be different (refund policies, advance notice requirements, and so on).

The example cancellations processor knows only how to process Gold cancellations, so you can discard everything else, as in the following example:

<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"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/integration
           <channel id="validated"/>
  <channel id="confirmed">
      <queue/>
  </channel>

  <channel id="rejected">
      <queue/>
  </channel>

  <gateway id="cancellationsGateway"
           service-interface="siia.booking.integration.cancellation.CancellationsGateway"
           default-request-channel="input"/>

<filter id="cancellationsFilter"
        input-channel="input"
        ref="cancellationsFilterBean"
        method="accept"
        discard-channel="rejected"
        output-channel="validated"/>

<beans:bean id="cancellationsFilterBean"
            class="siia.booking.integration.cancellation.CancellationRequestFilter">
     <beans:property name="pattern" value="GOLD[A-Z0-9]{6}+"/>
</beans:bean>

<service-activator
  id="goldCancellationsProcessor"
  input-channel="validated"
  ref="cancellationsService"
  method="cancel" output-channel="confirmed"/>

<beans:bean id="cancellationsService"
            class="siia.booking.domain.cancellation.StubCancellationsService"/>
</beans:beans>

Note the element used for inserting the filter between the gateway and the service activator that processes the cancellation requests. From a syntax perspective, its definition isn't very different from that of other message handlers discussed so far, such as the transformer and the service activator. It has an input channel for handling incoming messages, an output channel for forwarding messages, and delegates to a Spring bean for the actual processing logic. But it's semantically different. Messages don't suffer any transformation when they pass through this component; the same message instance that arrives on the input channel is forwarded to the validated channel if it passes the filtering test.

In our application, a cancellation request that can be processed by the service must contain a reservation code that corresponds to a Gold reservation. Of course, this tells nothing about whether a reservation with that code exists, and we won't try to do all the validation at this point (many things can be wrong with the request itself, and dealing with such errors is the responsibility of the cancellation service). But, at a minimum, you know that reservation codes have to conform to a certain standard pattern, and you can do a quick test for that. Here's how the implementation of the filtering class looks:

package siia.booking.integration.cancellation;

import siia.booking.domain.cancellation.CancellationRequest;

import java.util.regex.Pattern;

public class CancellationRequestFilter { 
    private Pattern pattern;

    public void setPattern(Pattern pattern) {
        this.pattern = pattern;
    }

    public boolean accept(CancellationRequest cancellationRequest) {
        String code = cancellationRequest.getReservationCode();
        return code != null && pattern.matcher(code).matches();
    }
}

As with the service activator and the transformer, you can implement the logic in a POJO (and we strongly recommend you do so). The filtering logic consists of a method that takes as argument the payload of a message (as in the previous example), one or more message headers (using the @Header/@Headers annotation), or even a full-fledged message, and returns a boolean indicating whether the message will pass through.

Where do rejected messages go?

In the simplest case, which is also the default, messages are just discarded (or, for a UNIX-based analogy, /dev/null). If you don't want them to be discarded, you have two other options:

  • You can specify a channel for the discarded messages. In this case, rejection is more a form of redirection, allowing the application to handle them further as regular messages. From the point of view of the framework, they're still messages and, therefore, subject to any handling a message on a channel can undergo.
  • You can instruct the framework to throw an exception whenever a message is rejected.

The framework allows both options to be enabled at the same time, but from a practical perspective, they're mutually exclusive. Nevertheless, when both options are active, the message is sent on the discarded messages channel before the exception is actually thrown (an important detail when a synchronous channel strategy is in place).

To illustrate, here's a more elaborate variant of the previous example, where rejected cancellation requests are redirected on a specific channel and from there are forwarded to an outbound notification system. Assuming the cancellation request contains enough information about the requester itself, so that a reply message can be sent, you can implement the filter this way:

<filter id="cancellationsFilter" input-channel="input"
        discard-channel="rejected"
        ref="cancellationsFilterBean" method="accept"
        output-channel="validated"/>
<bean id="cancelationsFilterBean"
      class="siia.booking.integration.cancellation.CancellationRequestFilter">
       <property name="pattern" value="GOLD[A-Z0-9]{6}+"/>
</bean>
<!-- other definitions -->
<channel id="input"/>
<channel id="validated"/>
<mail:header-enricher input-channel="rejected"
                      output-channel="mailTransformer">
    <mail:to expression="payload.requestor?.emailAddress"/>
</mail:header-enricher>

<transformer input-channel="mailTransformer"
             expression="payload.reservationCode + ' has been rejected'"
             output-channel="rejectionMail"/>

<mail:outbound-channel-adapter id="rejectionMail"
                               mail-sender="mailSender"/>

Or if you want to throw an exception instead, you can write this:

<filter id="cancellationsFilter" input-channel="input"
        throw-exception-on-rejection="true"
        ref="cancellationsFilterBean" method="accept"
        output-channel="validated"/>

This way, there's no need for a distinct bean to implement the decision logic. The advantage of using this filter definition is that the filtering logic can quickly be viewed in the context of the message flow. The disadvantage of using SpEL expressions directly is that they're harder to test in isolation from the filter itself, so you should take care to call out to the proper abstractions if the logic gets complicated.

Having two options on hand, when should you create a distinct implementation, and when should you use an inline expression? An inline expression is simple enough, and could be externalized easily (using, for example, a Property-PlaceholderConfigurer). But it's not flexible or reusable across the application. Our recommendation for making a decision in this case is to use expressions whenever the condition is based on the attributes of the message itself. In more complicated cases, when the decision involves a sophisticated algorithm, or the sender must consult with other collaborating components, you may want to create a standalone implementation and take advantage of Spring's dependency injection capabilities.

If you really want to have your cake and eat it too, there's a hybrid solution: use a SpEL expression, but delegate (part of) the logic to a Java object. One powerful and simple way to do this is to delegate the decision to the message payload or a header.

This requires the message to carry a domain object:

<filter id="cancellationsFilter" input-channel="input"
        discard-channel="rejected"
        expression="payload.isGold()"
        output-channel="validated"/>

Weighing the pros and cons of each option is something you'll have to do again for each situation. The framework will support whatever decision you make in the end.

Now that you have some tools, let's look at some uses for them.

Using filters for selective processing

Let's get a bit of perspective here: how do messages that don't satisfy the criteria to be processed get on the inbound channel, anyway? Wouldn't it be simpler if their producers didn't bother to send messages that won't be processed?

Often, filters are used in combination with publish-subscribe channels, allowing multiple consumers with different interests to subscribe to a single source of information and to process only the items they're really interested in. Such a solution doesn't preclude multiple components receiving a message at the same time, but the components have complete control over what they can and can't process.

Figure 2 shows such an example. Three different components with different interests subscribe to the same channel. It's much easier if a producer knows only about a single destination channel for its messages.

Figure 2 A publish-subscribe channel and filters combination for selective processing. The first subscriber is interested only in As; the second, only in Bs; and the third, in Bs and Cs.

Message filters can decide whether a message will be forwarded to a next channel.

If you decide to configure them with a channel for discarding messages, message filters will act as switches, choosing one channel or another depending on whether the message is accepted or discarded. In that case, they're a simplified form of a more general type of component that can decide from among multiple destinations where a message should go next. This component is the message router.

Summary

The publish-subscribe channel solution, on the other hand, is decentralized and open; the routing configuration is emerging as a sum of all the conditions defined on the individual filters, and new subscribers can be added without modifying any existing component. If your scenario calls for adding and removing consumers dynamically, the publish-subscribe channel solution is the best option. Otherwise, the centralized configuration provided by the router allows for easier maintenance.


Here are some other Manning titles you might be interested in:

Spring in Action, Third Edition

Spring in Action, Third Edition
Craig Walls

Spring Batch in Action

Spring Batch in Action
Thierry Templier, Arnaud Cogoluegnes, Gary Gregory, and Olivier Bazoud

Spring in Practice

Spring in Practice
Willie Wheeler, John Wheeler, and Joshua White


AttachmentSize
siiaimage001.png12.62 KB
siiaimage002.png32.51 KB
siiaimage003.jpg24.72 KB
siiaimage004.jpg24.63 KB
siiaimage005.png35.41 KB