Routing

Beside static-looking XML configuration, Spring Integration provides some dynamic mechanisms. One of them is routing which allows one message to be received by dynamically resolved message channel.

In this article we'll discover the feature of message routers. At the begin we'll describe theoretical aspect of routers, as a part of entreprise integration patterns. At the second part we'll try to define routing in Spring Integration. The last part'll contain some sample code using Spring Integration.

Routers in Entreprise Integration Patterns

Router can be thought in terms of special filter which analyzes message's content and decides where given message should be dispatched. The dispatch process is based on a set of criteria. For example, we can decide that messages with specific payload must be handled by channel A while the rest of message by channel B. We can do a lot more with the dispatching and send one message to two or more channels. Router sending messages to appropriated channels by analyzing the content of message initially sent, is called content-based router.

Filters, discovered in the article about filters in Spring Integration, are also considered as routers in entreprise integration patterns meaning. To remind quickly, the filters decide if given message can be consumed (= corresponds to expected conditions) by message channel or not. In the second case, discarded message can be simply rejected by throwing an error or sent into "trash" message channel, called discard message channel.

Another kind of router is recipient list. As the name indicates, it's about dispatching message to some predefined channels. These channels are configured in a waiting list called here recipient list.

Routers in Spring Integration

Spring Integration integrates well previously defined routers. We won't describe here filter but only two remaining types: content-based and recipient list routers. Basically, content-based routers can be defined only with configuration as also with Spring beans. Let's begin by the first ones. Spring's routers are in fact the implementations of org.springframework.integration.router.AbstractMessageRouter abstract class. They're created with special post processor for @Router annotation or with RouterFactoryBean factory class. One of classes implementing AbstractMessageRouter is AbstractMappingMessageRouter, also abstract. Its real implementations are used to find routes for messages by checking headers (HeaderValueRouter) or payloads (PayloadTypeRouter). We can also define our own router classes. In this case, we need also to specify method used to route message. This method can be annotated with @Router (processed with appropriated annotation post-processor) or defined directly in XML's method attribute.

It's a little bit different for recipient list router. It's represented by org.springframework.integration.router.RecpientListRouter class. It extends directly AbstractMessageRouter class and provides method defining recipient message channels:

@Override
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
  List<MessageChannel> channels = new ArrayList<MessageChannel>();
  for (Recipient recipient : this.recipients) {
    if (recipient.accept(message)) {
      channels.add(recipient.getChannel());
    }
  }
  return channels;
}

Note however that recipients can be dynamized with Spring Expression Language (SpEL) expression (selector-expression attribute). The expression is after evaluated against currently treated Message instance by RecipentListRouter's inner Recipent class:

private final MessageChannel channel;

private final MessageSelector selector;

public Recipient(MessageChannel channel) {
  this(channel, null);
}

public Recipient(MessageChannel channel, MessageSelector selector) {
  this.channel = channel;
  this.selector = selector;
}

public boolean accept(Message<?> message) {
  return (this.selector == null || this.selector.accept(message));
}

Example of routers in Spring Integration

To illustrate routers in Spring Integration we'll use the case of our shopping order. Every order considered as cheap will be dispatch to appropriated channel. Expensive order will be, in its turn, dispatched to another channel. Below you can find used configuration:

<context:annotation-config />
<context:component-scan base-package="com.waitingforcode"/>

<int:channel id="staticSender" />
<int:channel id="staticSenderNoFailing" />
<int:channel id="sender" />
<int:channel id="senderRouting" />
<int:channel id="senderRoutingSelector" />
<int:channel id="receiverCheapOrders">
  <int:queue />
</int:channel>
<int:channel id="receiverExpensiveOrders">
  <int:queue />
</int:channel>
<int:channel id="recipient1">
  <int:queue />
</int:channel>
<int:channel id="recipient2">
  <int:queue />
</int:channel>

<!-- case of static router, based on fixed configured values -->
<int:router id="staticRouter" input-channel="staticSender" expression="payload.id" resolution-required="true">
  <int:mapping value="30" channel="receiverExpensiveOrders" />
</int:router>
<int:router id="staticRouterNoFailing" input-channel="staticSenderNoFailing" expression="payload.id"
  resolution-required="false">
  <int:mapping value="30" channel="receiverExpensiveOrders" />
</int:router>

<!-- thanks to this router, we can send one message to a list of defined recipients -->
<int:recipient-list-router id="recipientRouter" input-channel="senderRouting">
  <int:recipient channel="recipient1" />
  <int:recipient channel="recipient2" />
</int:recipient-list-router>

<!-- recipient router list with dynamic selector expression -->
<int:recipient-list-router id="recipientWithSelector" input-channel="senderRoutingSelector">
  <int:recipient channel="recipient1" selector-expression="payload.finalPrice le 50" />
  <int:recipient channel="recipient2" selector-expression="payload.finalPrice gt 50" />
</int:recipient-list-router>


<!-- case of dynamic router which analyzes message's payload and chooses appropriate route -->
<int:router method="resolveOrderRoute" input-channel="sender" resolution-required="true" ref="orderChannelRouter" />

Unlike previously described routers, you can find here some supplementary configuration. For example, resolution-required attribute means that at least one recipient channel must be resolved. If set to true, MessagingException will be thrown when channel can't be resolved. Otherwise, standard MessageDeliveryException will be thrown. You can also find the use of customized router, represented by orderChannelRouter bean:

/**
 * Sample case for {@link Order} router.
 *
 * @author Bartosz Konieczny
 */
@Component
public class OrderChannelRouter {

  public String resolveOrderRoute(Order order) {
    order.calculateFinalPrice();
    String channel = "receiverCheapOrders";
    if (order.getFinalPrice() > 50d) {
      channel = "receiverExpensiveOrders";
    }
    return channel;
  }

}

As you can see if OrderChannelRouter, we make a simple check on order's final price to dispatch message to appropriated channel. You can observe it through following test cases:

/**
 * Test cases for routers.
 *
 * @author Bartosz Konieczny
 */
@ContextConfiguration(locations = "classpath:META-INF/router-sample.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class RouterTest {

    @Autowired
    @Qualifier("sender")
    private DirectChannel sender;

    @Autowired
    @Qualifier("staticSender")
    private DirectChannel staticSender;

    @Autowired
    @Qualifier("staticSenderNoFailing")
    private DirectChannel staticSenderNoFailing;

    @Autowired
    @Qualifier("senderRouting")
    private DirectChannel senderRouting;

    @Autowired
    @Qualifier("senderRoutingSelector")
    private DirectChannel senderRoutingSelector;

    @Autowired
    @Qualifier("receiverExpensiveOrders")
    private QueueChannel receiverExp;

    @Autowired
    @Qualifier("receiverCheapOrders")
    private QueueChannel receiverChe;

    @Autowired
    @Qualifier("recipient1")
    private QueueChannel recipient1;

    @Autowired
    @Qualifier("recipient2")
    private QueueChannel recipient2;

    @Test
    public void testStaticRouting() {
        Order order = new Order();
        order.setId(30); // will match configured route

        Message<Order> msg = MessageBuilder.<Order>withPayload(order).build();
        staticSender.send(msg, 2000);

        Message<?> received = receiverExp.receive(2000);
        assertEquals("Message should be sent to receiverCheapOrders channel", ((Order) received.getPayload()).getId(),
                order.getId());

        // test with Order containing not matching value
        order.setId(31);
        boolean wasMe = false;
        try {
            staticSender.send(msg, 2000);
        } catch (MessagingException me) {
            wasMe = true;
        }
        assertTrue("MessagingException should be thrown for non-resolvable channel (if resolution-required is set to true)",
                wasMe);

        // compare resolution-required="true" with resolution-required="false" - should throw MessageDeliveryException
        // "org.springframework.messaging.MessageDeliveryException: no channel resolved by router and no default output
        // channel defined"
        boolean wasMde = false;
        try {
            staticSenderNoFailing.send(msg, 2000);
        } catch (MessageDeliveryException mde) {
            wasMde = true;
        }
        assertTrue("MessageDeliveryException should be thrown for non-resolvable channel without resolution required", wasMde);
    }

    @Test
    public void testRecipientRouting() {
        Order order = new Order();
        order.setId(20000);
        Message<Order> msg = MessageBuilder.<Order>withPayload(order).build();

        senderRouting.send(msg, 200);

        Message<?> msgExpReceived = recipient1.receive(2000);
        assertEquals("Message should be sent to recipient1 channel", ((Order) msgExpReceived.getPayload()).getId(),
                order.getId());

        Message<?> msgCheapReceived = recipient2.receive(2000);
        assertEquals("Message should be sent to recipient2 channel", ((Order) msgCheapReceived.getPayload()).getId(),
                order.getId());
    }

    @Test
    public void testRecipientRoutingWithSelectors() {
        Product milk = new Product();
        milk.setName("milk");
        milk.setPrice(30);
        Order order = new Order();
        order.setId(300);
        order.getProducts().add(milk);
        order.calculateFinalPrice();

        Message<Order> msg = MessageBuilder.<Order>withPayload(order).build();
        senderRoutingSelector.send(msg, 200);

        Message<?> msgCheapReceived = recipient1.receive(2000);
        assertEquals("Message should be sent to recipient1 channel", ((Order) msgCheapReceived.getPayload()).getId(),
                order.getId());

        Message<?> msgExpReceived = recipient2.receive(2000);
        assertNull("Message should be sent only for 1st recipent because final price is lower than 50", msgExpReceived);
    }

    @Test
    public void testDynamicRouting() {
        Order order = new Order();
        order.setId(20);
        Product milk = new Product();
        milk.setName("milk");
        milk.setPrice(39.99d);
        Product coffee = new Product();
        coffee.setName("coffee");
        coffee.setPrice(10.01d);
        order.addProduct(milk);
        order.addProduct(coffee);

        Message<Order> msg = MessageBuilder.<Order>withPayload(order).build();
        sender.send(msg, 2000);

        Message<?> msgExpReceived = receiverExp.receive(2000);
        assertNull("Message shouldn't be sent to receiverExpensiveOrders channel", msgExpReceived);

        Message<?> msgCheapReceived = receiverChe.receive(2000);
        assertEquals("Message should be sent to receiverCheapOrders channel", ((Order) msgCheapReceived.getPayload()).getId(),
                order.getId());
    }
}

In this article we learned a little about message routers. We discovered that routers can be defined on message's content (content based routers) or with fixed recipients (recipient list). However, both can be dynamic thanks to SpEL expressions defined to evaluate payloads or headers and forward message to appropriated channel.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!