Web services programming tips and tricks: Simple, practical Web service design patterns, Part 3

Creating flexible Web service implementations with the Router pattern

This third tip in a series continues the short series of discussions focusing on the application of well-defined and proven Web application design strategies to the world of Web services with an exploration of the Router pattern.

Share:

James Snell (jasnell@us.ibm.com), Software Engineer, EMC

Author photoJames Snell is a member of the IBM Emerging Technologies Toolkit team and has spent the past few years focusing on emerging Web services technologies and standards. He maintains a weblog on developerWorks focused on emerging technologies.


developerWorks Contributing author
        level

23 November 2004

Also available in Russian

The few installments of this series have illustrated some very simple and straightforward alternative techniques for creating Web service implementations using known, proven design patterns. So far, I've looked at asynchronous request/response using message queues and business logic encapsulation using the Command pattern. This time around, let’s explore the creation of a service that acts as a simple router, dispatching requests to specific business logic components based on the value of individual input parameters. One thing that you'll notice as you're going through this tip is that the code is rather simplistic, which is exactly the point. The goal is not to introduce any revolutionarily new design concepts or coding techniques. Rather, what I’m trying to do with this series is make the point that with some very basic steps, you can create very flexible service implementations capable of solving a broad variety of problems.

The Router pattern

The Router pattern centers around the simple concept of routing requests to specific pieces of business logic based on some defined criteria. In the world of programming, this pattern is best illustrated by such constructs as switch-case and if-then-else blocks. The bottom line is that I'm not talking about anything all that complicated. See Figure 1 below.

Figure 1. The Router pattern
The Router pattern

The router is essentially a transparent component from the client's point of view. It is responsible for receiving a request and figuring out where it is supposed to go for processing. Some defined set of criteria determines where a router sends the message. The routing criteria can be static or dynamic. Static routing implies that the routing rules never change. The same message is sent to the same business component each time regardless of current conditions. Dynamic routing implies that some set of current conditions affect the specific route a message takes. The same message, if sent multiple times, might or might not be sent to the same business component for processing.

The single most important thing to keep in mind is that the client should have absolutely no idea what routing model you are using. In fact, ideally, the client should never even need to know that routing is taking place -- this leads to the very important consideration involving session or state management.

If a client sends a series of messages or invokes a number of operations against a Web service, you cannot guarantee that a routing-based implementation uses a single business component to process each of those messages or requests. In addition, you cannot assume that the routed components have any idea that the client is not directly accessing them, or that they are routing to a collection of business components. The simplest thing to do is to not allow Web services to maintain an internal state of any kind, but this isn’t always possible or desirable. The more complicated solution is to work out some mechanism of allowing a business component to store session state externally, using some form of shared session service that all of the business components can access. For simple, Web-based applications, where all of the business components are encapsulated into a single application running on single machine, this is an easy thing to do. However, distributed business components across multiple machines can create problems. Some application servers provide distributed session services that you can leverage, but you need to plan ahead and design your applications accordingly to make appropriate use of such services.

Figure 2. Router pattern with a shared session context
Router Pattern with a Shared Session Context

Figure 2 above illustrates the implementation of HTTP sessions within most Web application servers. A request listener receives HTTP messages and forwards those requests to some identified business component (a Java™ servlet, a JSP page, and so forth), while providing a shared session context that is maintained independently of the specific component a request is routed to.

Another important aspect of the Router pattern is the notion that the deployment of the business components being routed to is independent of the deployment of the router. In other words, the business components are distinctly separate pieces of the application that could very well be deployed on:

  • Completely different servers
  • On the same server
  • Within the same network environment
  • Through some firewall boundary

See Figure 3 below.

Figure 3. Router pattern using intermediate queues
Router Pattern using Intermediate Queues

Before you start looking at the actual code, please note that the routers might be unidirectional or bidirectional. That is, the router that is responsible for dispatching requests to appropriate business components might or might not also be responsible for returning potential responses to those requests back to the client. For instance, suppose you want to use an asynchronous communication model, you could easily use the mechanisms described in the first installment of this series of tips to allow a client to pick up response messages directly from a waiting queue. Alternatively, upon delivering the request to a given component, the router can decide to sit around and wait for a response before returning it to the client. The behavior is entirely dependent on what the developer is attempting to accomplish.


Implement a Web services router

To illustrate a simple application of the Router pattern, you’re going to create a Web service that exposes a single operation that, depending on the specific value of the input parameters, will either convert a provided text string to either all caps or all lower case.

The various components of your application deploy within a single Web application but make use of intermediate Java Messaging Service (JMS) queues. Session support will not be enabled simply because it is not necessary to illustrate the fundamentals of the pattern.

To get started with the example, you need to have a J2EE Web Application Server and a JMS provider available. While creating the sample code, I used the OpenJMS open source JMS provider available from Sourceforge.net (see Resources).

Once you have your operating environment set up, it is time to get started writing the code. As one could imagine, for our example (see Listing 1), the code is not very complicated. Let’s start with the two business components responsible for processing the request messages. Each is implemented as an HTTP servlet that is initialized on server startup and that implements the standard JMS MessageListener interface. Each servlet listens on a specific JMS queue for request messages, and upon receiving a request will perform its specified action, placing response messages on a single shared outbound response queue. With the exception that one of the listeners converts the input string to uppercase, while the second converts the string to lowercase, the code for the listeners is exactly the same.

Listing 1. UppercaseListenerServlet.java and LowercaseListenerServlet.java
package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public class UppercaseListenerServlet 
  extends HttpServlet 
  implements MessageListener, Servlet {

  private Context context;
  private QueueConnection connection;
  private QueueSession session;
  private Queue queue;
  private QueueReceiver receiver;

  public void init() 
    throws ServletException {

    super.init();

    try {
      context = JNDIHelper.getInitialContext();
      connection = JNDIHelper.getQueueConnection(context);
      session = JNDIHelper.getQueueSession(connection);
      queue = JNDIHelper.getQueue(context, "upperqueue");
      receiver = JNDIHelper.getQueueReceiver(session, queue);
      receiver.setMessageListener(this);
      System.out.println("UppercaseListenerServlet is listening");
    } catch (Exception e) {
      System.out.println("UppercaseListenerServlet init Error");
      e.printStackTrace(System.out);
    }

  }
  
  public void onMessage(Message message) {
    try {
      if (message instanceof TextMessage) {
        TextMessage txtMessage = (TextMessage) message;
        String txt = txtMessage.getText().toUpperCase();
        TextMessage response = session.createTextMessage();
        response.setJMSCorrelationID(txtMessage.getJMSCorrelationID());
        response.setText(txt);
        Queue queue = JNDIHelper.getQueue(context, "queue6");
        JNDIHelper.getQueueSender(session, queue).send(response);
      }
    } catch (Exception e) {
      System.out.println("UppercaseListenerServlet onMessage Error");
      e.printStackTrace(System.out);
    }
  }

}

package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public class LowercaseListenerServlet 
  extends HttpServlet 
  implements MessageListener, Servlet {

  private Context context;
  private QueueConnection connection;
  private QueueSession session;
  private Queue queue;
  private QueueReceiver receiver;

  public void init() 
    throws ServletException {

    super.init();

    try {
      context = JNDIHelper.getInitialContext();
      connection = JNDIHelper.getQueueConnection(context);
      session = JNDIHelper.getQueueSession(connection);
      queue = JNDIHelper.getQueue(context, "lowerqueue");
      receiver = JNDIHelper.getQueueReceiver(session, queue);
      receiver.setMessageListener(this);
      System.out.println("LowercaseListenerServlet is listening");
    } catch (Exception e) {
      System.out.println("LowercaseListenerServlet init Error");
      e.printStackTrace(System.out);
    }

  }
  
  public void onMessage(Message message) {
    try {
      if (message instanceof TextMessage) {
        TextMessage txtMessage = (TextMessage) message;
        String txt = txtMessage.getText().toLowerCase();
        TextMessage response = session.createTextMessage();
        response.setJMSCorrelationID(txtMessage.getJMSCorrelationID());
        response.setText(txt);
        Queue queue = JNDIHelper.getQueue(context, "responsequeue");
        JNDIHelper.getQueueSender(session, queue).send(response);
      }
    } catch (Exception e) {
      System.out.println("LowercaseListenerServlet onMessage Error");
      e.printStackTrace(System.out);
    }
  }

}

The router, whose code you'll see shortly (see Listing 2), exposes an operation called process that accepts two parameters. The first parameter identifies the operation that the client requests to be performed. The choice of value is upper or lower. The second parameter is the input string that either converts to uppercase or lowercase characters. Upon receiving the process request, the router places the input text string on either the upperqueue or lowerqueue, depending on the selected action. The appropriate listener picks up the message and handles the request, placing the converted text on responsequeue. The router grabs the message from responsequeue and returns the converted text back to the waiting client, as shown in Figure 4.

Figure 4. The RouterService example application
The RouterService example application
Listing 2. RouterService.java
package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;


public class RouterService {

  public String process(String action, String target) {
    String response = null;
    try {
      Context context = JNDIHelper.getInitialContext();
      QueueConnection connection = JNDIHelper.getQueueConnection(context);
      QueueSession session = JNDIHelper.getQueueSession(connection);
      String queueName = 
      ("upper".equalsIgnoreCase(action)) ? "upperqueue" : "lowerqueue";
      Queue queue = JNDIHelper.getQueue(context, queueName);
      QueueSender sender = JNDIHelper.getQueueSender(session,queue);
      
      TextMessage message = session.createTextMessage();
      String corrID = Long.toString(System.currentTimeMillis());
      message.setJMSCorrelationID(corrID);
      message.setText(target);
      
      sender.send(message);
      
      Queue rqueue = JNDIHelper.getQueue(context, "responsequeue");
      String selector = "JMSCorrelationID = '" + corrID + "'";
      QueueReceiver rec = JNDIHelper.getQueueReceiver(session, rqueue, selector);
      TextMessage resp = (TextMessage)rec.receive(10 * 1000);
      response = resp.getText();
    } catch (Exception e) {
      response = e.getMessage();
    }
    return response;
  }

}

Both of the listener servlets and the RouterService class make use of a shared utility class used to encapsulate the various minute details of working with the JMS API. This utility class, for convenience, is shown in Listing 3.

Listing 3. JNDIHelper.java
package com.ibm.developerworks.wspattern.three;

import java.util.Hashtable;

import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;


public class JNDIHelper {

  private static Context context;

  public static Context getInitialContext() 
    throws NamingException {
      if (context == null) {
        Hashtable properties = new Hashtable();
        properties.put(
          Context.INITIAL_CONTEXT_FACTORY, 
          "org.exolab.jms.jndi.InitialContextFactory");
        properties.put(
          Context.PROVIDER_URL,
          "rmi://localhost:1099");
        context = new InitialContext(properties);
      }
      return context;
  }

  public static QueueConnection getQueueConnection(
    Context context) 
      throws NamingException, 
             JMSException {
        QueueConnectionFactory factory = 
          (QueueConnectionFactory) context.lookup(
            "JmsQueueConnectionFactory");
        QueueConnection connection = factory.createQueueConnection();
        connection.start();
        return connection;
  }

  public static QueueSession getQueueSession(QueueConnection connection) 
    throws JMSException {
      QueueSession session = 
        connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      return session;
  }

  public static Queue getQueue(Context context, String name)
    throws NamingException {
      Queue queue = (Queue) context.lookup(name);
      return queue;
  }

  public static QueueSender getQueueSender(
    QueueSession session, 
    Queue queue) 
      throws JMSException {
        QueueSender sender = session.createSender(queue);
        return sender;
  }

  public static QueueReceiver getQueueReceiver(
    QueueSession session,
    Queue queue) 
      throws JMSException {
        QueueReceiver receiver = session.createReceiver(queue);
        return receiver;
  }

  public static QueueReceiver getQueueReceiver(
    QueueSession session,
    Queue queue,
    String selector) 
      throws JMSException {
        QueueReceiver receiver = session.createReceiver(queue, selector);
        return receiver;
  }
    
}

Once you create the RouterService.java, the final step is to deploy it as a Web service. How this is done is depends on the application server and development tools you are using. For the example, I used the IBM®WebSphere® Application Server Version 5.1 and the WebSphere Studio Application Developer toolset. Regardless of the toolset, the RouterService class becomes the underlying implementation class of the Web service. An instance of the router receives all requests coming through the service interface (see Listing 4 below).

Listing 4. The RouterService WSDL description
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions 
  targetNamespace="http://three.wspattern.developerworks.ibm.com" 
  xmlns:impl="http://three.wspattern.developerworks.ibm.com" 
  xmlns:intf="http://three.wspattern.developerworks.ibm.com" 
  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
  xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <wsdl:types>
  <schema 
    targetNamespace="http://three.wspattern.developerworks.ibm.com" 
    xmlns="http://www.w3.org/2001/XMLSchema" 
    xmlns:impl="http://three.wspattern.developerworks.ibm.com" 
    xmlns:intf="http://three.wspattern.developerworks.ibm.com" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <element name="action" nillable="true" type="xsd:string"/>
   <element name="target" nillable="true" type="xsd:string"/>
   <element name="processReturn" nillable="true" type="xsd:string"/>
  </schema>
 </wsdl:types>

   <wsdl:message name="processRequest">
      <wsdl:part name="action" type="xsd:string"/>
      <wsdl:part name="target" type="xsd:string"/>
   </wsdl:message>

   <wsdl:message name="processResponse">
      <wsdl:part name="processReturn" type="xsd:string"/>
   </wsdl:message>

   <wsdl:portType name="RouterService">
      <wsdl:operation name="process" parameterOrder="action target">
         <wsdl:input message="intf:processRequest" name="processRequest"/>
         <wsdl:output message="intf:processResponse" name="processResponse"/>
      </wsdl:operation>
   </wsdl:portType>

   <wsdl:binding name=
   "RouterServiceSoapBinding" type="intf:RouterService">
      <wsdlsoap:binding style=
      "rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
      <wsdl:operation name="process">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="processRequest">
            <wsdlsoap:body 
              namespace="http://three.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>
         <wsdl:output name="processResponse">
            <wsdlsoap:body 
              namespace="http://three.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>
      </wsdl:operation>
   </wsdl:binding>

   <wsdl:service name="RouterServiceService">
      <wsdl:port binding="intf:RouterServiceSoapBinding" name="RouterService">
         <wsdlsoap:address 
           location="http://localhost:9080/WSPattern3/services/RouterService"/>
      </wsdl:port>
   </wsdl:service>
</wsdl:definitions>

Once you implement the service, deploy your Web application and start your Java Naming and Directory Interface (JNDI) service provider and application server (in that order or the example won’t work). You can now test out the service (see Listing 5 below).

Listing 5. An example message exchange with the RouterService
Request
<SOAP-ENV:Envelope 
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SOAP-ENV:Body>
    <m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
      <action>upper</action>
      <target>this is a test</target>
    </m:process>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Response
<soapenv:Envelope 
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header/>
  <soapenv:Body>
    <p317:processResponse xmlns:p317=
    "http://three.wspattern.developerworks.ibm.com">
      <processReturn>THIS IS A TEST</processReturn>
    </p317:processResponse>
  </soapenv:Body>
</soapenv:Envelope>

Request
<SOAP-ENV:Envelope 
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SOAP-ENV:Body>
    <m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
      <action>lower</action>
      <target>THIS IS A TEST</target>
    </m:process>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Response
<soapenv:Envelope 
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header/>
  <soapenv:Body>
    <p317:processResponse xmlns:p317=
    "http://three.wspattern.developerworks.ibm.com">
      <processReturn>this is a test</processReturn>
    </p317:processResponse>
  </soapenv:Body>
</soapenv:Envelope>

Closing points

To wrap up, let’s go over a couple of closing points about the example presented here that you should consider. First, in your example, you used one of the input parameters to specify the activity that you wished to perform. As an alternative, you could have made use of the WS-Addressing specification's action SOAP header to specify the activity.

Further, in this example, the activity was an explicit element of the request. In many router implementations; however, the criteria upon which you base the routing decision might be far more implicit than a dedicated parameter. For instance, a purchase order processing service might decide to route messages according to the total value of the order, or upon the identity of the purchaser, and so forth. As indicated earlier, these criteria might even be dynamic, able to change at run time, or based on some defined set of business parameters that might or might not be hard coded into the application logic.

Many current Web service implementations tend to hard code business logic directly into the service class that backs the WSDL defined Web service interface. Doing so, however, makes the Web service brittle and does not allow an application to take full advantage of the philosophy of loose coupling that has driven the success of the Web services concept. While the ideas presented in this series are simple, they can go a long way towards vastly improving the design of your service-oriented applications.


Download

DescriptionNameSize
EAR filews-pattern3.zip1397 KB

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into SOA and web services on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=SOA and web services
ArticleID=31822
ArticleTitle=Web services programming tips and tricks: Simple, practical Web service design patterns, Part 3
publish-date=11232004