ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Enhancing Web Services Infrastructures with JMS

by Gunnison Carbone
06/19/2002

Web services are revolutionizing the Internet by enabling applications to speak a common language: XML. Under the Web services paradigm, a single application can tap into the services of millions of applications scattered throughout the Internet. The potential of this is enormous. Web services allow cooperation, communication, and integration on a global scale.

While Web services technology enables the execution of remote functions and messaging, it does not provide a robust infrastructure for handling information. An enterprise-class application that communicates with Web services must ensure that the data can be handled appropriately.

When employing Web services, one must ask questions like: Can our application scale to increased messaging demands? If our application crashes, is the Web services data lost? How do we efficiently replicate similar Web services clients across several applications? How do we connect our Web-services-facing applications to back-end systems?

As an example, let's look at a hypothetical e-commerce application that receives and processes orders through Web services. If the Web services data is not persisted, the data will be lost if the application fails. If the system is inundated with orders -- spiking from, say, a few thousand transactions a day to 10 or 100 times that amount -- it must be able to handle the increased load. If the application needs to interface with a backend system (i.e. an inventory management system) to determine if a product is in stock, there must be a bridge for efficient and reliable communication.

These problems are created but unfortunately not solved through the Web services architecture. Web services must be combined with additional technology for robust enterprise messaging. One very strong candidate is the Java Message Service (JMS). JMS provides a reliable, scalable, and loosely coupled architecture for messaging. The combination of Web services with JMS creates an architecture that can communicate across the Internet, reliably handle data, and integrate with backend systems.

A first-generation Web-services-enabled application contains or directly interfaces with a client that communicates with Web services, as depicted in Figure 1. This architecture enables the application to find and communicate with remote systems, but does not implement data relability, scalability, and reusage of Web service client logic. The addition of JMS creates a second generation for architecting Web services systems, as shown in Figure 2. JMS decouples the application from the task of Web services messaging. Applications communicate directly or through an adapter to the JMS server.

Diagram.
Figure 1. Tightly coupled architecture.


Diagram.
Figure 2. Loosely coupled architecture with JMS.

In this new architecture, hybrid JMS and Web services clients handle the bulk of the messaging duties. Information is passed through the JMS server, which natively handles issues like failover, load balancing, and guaranteed message delivery. By decoupling the Web services client from the application, several applications can effortlessly reuse a single Web services client. Decoupling makes it a simpler process to upgrade the Web service as inevitable software changes occur. Additionally, an application that becomes busy will have its Web services data automatically queued in the JMS server until it is able to process the messages. In a tightly coupled architecture, the application's Web services piece would have to wait until the application is ready to begin processing data.

Related Reading

Java and SOAP
By Robert Englander

A Second-generation Web service

To demonstrate this second-generation Web services architecture, we'll develop an example using several popular open source tools. This example is a simplified portion of an e-commerce system. It is composed of four Java classes:

To build our system, we'll utilize the following open source tools:

Figure 3 displays the basic architecture of the system.

Diagram
Figure 3. System architecture of a second-generation Web service.


JMSClientSendReceive Class

We'll start by looking at the JMSClientSendReceive class.

This class implements a JMS queue sender and receiver. If you're unfamiliar with JMS, take a look at this introductory article on JMS.

package org.open3.soap;

import java.util.*;

import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;

public class JMSClientSendReceive implements MessageListener {
    
  //contants for JMS  
  public final static String CTX_FACTORY = "org.open3.jndi.lite.LiteContextFactory";
  public static final String QCF_NAME = "QueueConnectionFactory";
  public final static String TO_WS_DEST_NAME = "toWS@open3soap";
  public final static String FROM_WS_DEST_NAME = "fromWS@open3soap";
  
 
  protected String  _configFile = "config/config.xml";
  protected String _configName = "standard";
  protected String _parser = "org.apache.crimson.parser.XMLReaderImpl";
  protected boolean _verbose = false; 
  protected QueueConnectionFactory _qcf = null;
  protected QueueConnection connection = null;
  protected Queue _queueToWS = null;
  protected Queue _queueFromWS = null;
  protected QueueSession sessionReceiver = null;
  protected QueueSession sessionSender = null;
  protected QueueReceiver receiver = null;
  protected QueueSender sender = null;
  
  //constants for xml document
  public static String XML_ORDER_OPEN_TAG = "<order>";
  public static String XML_ORDER_CLOSE_TAG = "</order>";
  public static String XML_WEIGHT_OPEN_TAG = "<shippingWeight>";
  public static String XML_WEIGHT_CLOSE_TAG = "</shippingWeight>";
  public static String XML_DISTANCE_OPEN_TAG = "<destinationDistance>";
  public static String XML_DISTANCE_CLOSE_TAG = "</destinationDistance>";
  public static String XML_COST_OPEN_TAG = "<shippingCost>";
  public static String XML_COST_CLOSE_TAG = "</shippingCost>";

  //Constructor
  public JMSClientSendReceive() {

  }
  
  public void init(String receiveFromQueue, String sendToQueue) {
        try {
            
            InitialContext initContext = null;
            
            //InitialContext through JNDI
            Properties props = new Properties();  
            props.put(Context.INITIAL_CONTEXT_FACTORY, CTX_FACTORY);
            props.put("org.open3.xstp.server.ConfigFile", _configFile);
            props.put("org.open3.jms.ConfigName", _configName);
            props.put("org.open3.xstp.server.SAXParser", _parser);

            initContext = new InitialContext(props);

            //get the Queue Connection factory
            _qcf = (QueueConnectionFactory)initContext.lookup(QCF_NAME);

            //get the queue destinations
            _queueToWS = (Queue)initContext.lookup(receiveFromQueue);
            _queueFromWS = (Queue)initContext.lookup(sendToQueue);

            connection = _qcf.createQueueConnection();

            //create a receiver and sender session 
            sessionReceiver = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
            sessionSender = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);

            //create the receiver and sender
            receiver = sessionReceiver.createReceiver(_queueToWS);
            sender = sessionSender.createSender(_queueFromWS);

            //set to process onMessage events
            receiver.setMessageListener(this);

            //start delivery of incoming messages
            connection.start();

        }
        catch (Exception e) {
            System.out.println("JMSClientSendReceive.init Error: " + e.toString());
            e.printStackTrace();
        }
  }
  
  public void cleanUp() {
        //free up system resources
        try {
            sender.close();
            sessionReceiver.close();
            sessionSender.close();
            connection.close();
        } 
        catch(Exception e) {
            System.out.println("JMSClientSendReceive.cleanUp Error: " + e.toString());
        }
  }
  
  public void onMessage(javax.jms.Message msg) { }
  
  public boolean sendJMSMessage(String msgContent) {
      //sends a JMS TextMessage back to the JMS server
      boolean sendOK = true;
      
      System.out.println("ShippingClient: sending JMS message: " + msgContent);
      try {
        Message msg = sessionSender.createTextMessage(msgContent);
        sender.send(msg);
      }
      catch (Exception e) {
           System.out.println("ShippingClient.sendJMSMessage Error: " + e.toString());
           e.printStackTrace();
           sendOK = false;
      }
      return sendOK;   
  }
   
}

This class defines constants and variable fields for connecting to and using the JMS API.

public final static String CTX_FACTORY = "org.open3.jndi.lite.LiteContextFactory";
public static final String QCF_NAME = "QueueConnectionFactory";
public final static String TO_WS_DEST_NAME = "toWS@open3soap";
public final static String FROM_WS_DEST_NAME = "fromWS@open3soap";

These constants define the JNDI context factory and the names of the QueueConnection factory and queue destinations. All of the system's messages travel through two queues, toWS@open3soap and fromWS@open3soap. The toWS prefix refers to messages headed toward the Web service, and fromWS handles messages coming back from the Web service. The open3soap suffix is the namespace of the destination. We must configure the destinations in Open3.org JMS destinations.xml file. The destinations.xml file is under the config/ directory of the base directory. Add these two lines:

<destination type="queue" name="toWS" namespace="open3soap"/>
<destination type="queue" name="fromWS" namespace="open3soap"/>

The JMSClientSendReceive class also contains the elements for the XML document that forms the body of our messages. You wouldn't necessarily want to define your XML tags as String constants, but we've done so for simplicity:

public static String XML_ORDER_OPEN_TAG = "<order>";
public static String XML_ORDER_CLOSE_TAG = "</order>";
public static String XML_WEIGHT_OPEN_TAG = "<shippingWeight>";
public static String XML_WEIGHT_CLOSE_TAG = "</shippingWeight>";
public static String XML_DISTANCE_OPEN_TAG = "<destinationDistance>";
public static String XML_DISTANCE_CLOSE_TAG = "</destinationDistance>";
public static String XML_COST_OPEN_TAG = "<shippingCost>";
public static String XML_COST_CLOSE_TAG = "</shippingCost>";

The init() function connects the client to the server and sets up the sender and receiver sessions. In implementing the MessageListener interface, we utilize the event listener model for receiving messages. JMS specifies that only one thread may operate on a session at a time, and therefore we create separate sessions for the sender and receiver:

sessionReceiver = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
sessionSender = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);

The AUTO_ACKNOWLEDGE mode is utilized. Messages are automatically acknowledged when received by the client.

This class also defines an empty onMessage() method that is overridden by classes that extend JMSClientSendReceive:

public void onMessage(javax.jms.Message msg) { }

The class implements a general sendJMSMessage method that takes a String parameter and utilizes this to build a JMS TextMessage. The system strictly utilizes JMS TextMessages to encapsulate the XML documents.

OrderApplication Class

Our next class is OrderApplication, which simulates an application for processing orders:

package org.open3.soap;

import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;

public class OrderApplication extends JMSClientSendReceive{

    /** Creates new OrderApplication */
    public OrderApplication() {
         super.init(FROM_WS_DEST_NAME, TO_WS_DEST_NAME);
        
        //create and send a message
        String msgOrder = XML_ORDER_OPEN_TAG + 
            XML_WEIGHT_OPEN_TAG + "45.50" + XML_WEIGHT_CLOSE_TAG +
            XML_DISTANCE_OPEN_TAG + "500" + XML_DISTANCE_CLOSE_TAG +
            XML_ORDER_CLOSE_TAG;
        
        boolean msgOK = sendJMSMessage(msgOrder);
    }

    public void onMessage(javax.jms.Message msg) {
        //a message has been received from the JMS server, this message
        //contains the 

        try{
            //get the xml data from the TextMessage
            String xmlData = ((TextMessage)msg).getText();

            String strShipCost = xmlData.substring(
                xmlData.indexOf(XML_COST_OPEN_TAG) + XML_COST_OPEN_TAG.length(),
                xmlData.indexOf(XML_COST_CLOSE_TAG));

           System.out.println("Shipping Cost=" + strShipCost);
        }
        catch (Exception e){
            System.out.println("OrderApplication.onMessage Error: " + e.toString());
        }
   }
    
    public static void main(String[] args) throws Exception {
        OrderApplication app = new OrderApplication();
    }
  
}

OrderApplication's constructor calls the parent's init() function to establish its JMS connections. Notice that the receiver is connected to FROM_WS_DEST_NAME, which is the queue for messages coming from the Web service, and the sender to TO_WS_DEST_NAME, which is the queue for messages going out to the Web service:

public OrderApplication() {
    super.init(FROM_WS_DEST_NAME, TO_WS_DEST_NAME);
        
    //create and send a message
    String msgOrder = XML_ORDER_OPEN_TAG + 
        XML_WEIGHT_OPEN_TAG + "45.50" + XML_WEIGHT_CLOSE_TAG +
        XML_DISTANCE_OPEN_TAG + "500" + XML_DISTANCE_CLOSE_TAG +
        XML_ORDER_CLOSE_TAG;
        
    boolean msgOK = sendJMSMessage(msgOrder);
}

The constructor also creates and sends an XML message to be delivered to the toWS@open3soap queue. The body of message is:

<order>
    <shippingWeight>45.50</shippingWeight>
    <destinationDistance>500</destinationDistance>
</order>

This document specifies two fields, shippingWeight and destinationDistance. These fields are utilized by the Web service to calculate a shipping cost for the order.

The onMessage method is called when a message has been returned from the Web service client. This message contains a document that appears as:

<order>
    <shippingCost>39.625</shippingCost>
</order>

The onMessage method extracts the shipping cost from the document and prints it.

ShippingClient

On the other side of the JMS server is the ShippingClient.

This client is a hybrid JMS and Web services client. It extends JMSClientSendReceive and also implements functionality for connecting to, and making a call to, a Web service. The source code for the ShippingClient is:

package org.open3.soap;

import java.net.*;
import java.util.*;

import org.apache.soap.*;
import org.apache.soap.rpc.*;

import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;

import org.open3.soap.*;

public class ShippingClient extends JMSClientSendReceive {

  //constants for Web Service
  public static String SERVICE_URL = "http://localhost:8080/soap/servlet/rpcrouter";
  public static String TARGET_URI = "urn:open3soap";
        
  //Constructor
  public ShippingClient() {
      super.init(TO_WS_DEST_NAME, FROM_WS_DEST_NAME);
  }
  
  public void onMessage(javax.jms.Message msg) {
        //a message has been received from the JMS server, need to process this
        //and send it to the Web service 

        try{

            //get the xml data from the TextMessage
            String xmlData = ((TextMessage)msg).getText();

            System.out.println("ShippingClient: received JMS message: " + xmlData);
            
            //parse the data
            //for simplicity the parsing is manual, but we could use SAX or DOM
            String strShipWeight = xmlData.substring(
                xmlData.indexOf(XML_WEIGHT_OPEN_TAG) + XML_WEIGHT_OPEN_TAG.length(),
                xmlData.indexOf(XML_WEIGHT_CLOSE_TAG));

           String strDestDistance = xmlData.substring(
                xmlData.indexOf(XML_DISTANCE_OPEN_TAG) + XML_DISTANCE_OPEN_TAG.length(),
                xmlData.indexOf(XML_DISTANCE_CLOSE_TAG));

           //****DELETE
           System.out.println("shipWeight=" + strShipWeight);
           System.out.println("destDistance=" + strDestDistance);

           //type converstions
           Double shipWeight = new Double(strShipWeight);
           Integer destDistance = new Integer(strDestDistance);
        
           boolean processOK = callWebService(shipWeight, destDistance);
        }
        catch (Exception e){
            System.out.println("ShippingClient.onMessage Error: " + e.toString());
        }
  }
      
  public boolean callWebService(Double shipWeight, Integer destDistance) {
    
    boolean callOK = true;  
    
    System.out.println("ShippingClient: calling Web service: (" + shipWeight + 
        ", " + destDistance + ")");
    
    try {
    
        // Construct the RPC Call
        Call call = new Call();
        call.setTargetObjectURI(TARGET_URI);
        call.setMethodName("calculateShippingCost");
        call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC);

        Vector params = new Vector();
        params.addElement(new Parameter("shippingWeight", Double.class, shipWeight, null));
        params.addElement(new Parameter("p2", Integer.class, destDistance, null));
        call.setParams (params);

        // peform the call
        URL url = new URL (SERVICE_URL);
        Response response = call.invoke(url, "" );

        // response from the call
        if ( response.generatedFault() ) {
          Fault fault = response.getFault ();
          System.out.println("ShippingClient.callWebService Error");
          System.out.println("FaultCode = " + fault.getFaultCode());
          System.out.println("FaultString = " + fault.getFaultString());
          callOK = false;
        }
        else {
          Parameter returnValue = response.getReturnValue();
          callOK = constructXMLResponse(((Double)returnValue.getValue()).doubleValue());
        }
    }
    catch (Exception e) {
        System.out.println("ShippingClient.callWebService Error: " + e.toString());
        callOK = false;
    }
    
    return callOK;
  }
  
  public boolean constructXMLResponse(double shippingCost) {
      //builds the XML string for the response from the Web service
      String responseXML = XML_ORDER_OPEN_TAG + XML_COST_OPEN_TAG +
        shippingCost + XML_COST_CLOSE_TAG + XML_ORDER_CLOSE_TAG;
  
      System.out.println("ShippingClient: response from Web service. XML: " + 
        responseXML);
      
      return sendJMSMessage(responseXML);
  }
  
  public static void main(String[] args) throws Exception {

     ShippingClient client = new ShippingClient();
  }
}

The Web service location and URI are defined in constant fields:

public static String SERVICE_URL = "http://localhost:8080/soap/servlet/rpcrouter";
public static String TARGET_URI = "urn:open3soap";

The class' constructor calls the parent's init() to set up its JMS client functionality. Notice that the queue destinations for the receiver and sender are the opposite of those defined in the OrderApplication application. The ShippingClient's receiver retrieves messages from the toWS@open3soap queue and sends messages to the fromWS@open3soap queue.

super.init(TO_WS_DEST_NAME, FROM_WS_DEST_NAME);

The ShippingClient waits until an onMessage() event is generated. Upon onMessage(), the method processes the message and extracts the shippingWeight and destinationDistance fields. These fields are transformed to Double and Integer objects, respectively. These objects are utilized as parameters when calling the Web service.

The callWebService method establishes the connection and makes the RPC call to the Web service. The data that is returned from the Web service is processed and converted to an XML document:

Parameter returnValue = response.getReturnValue();
callOK = constructXMLResponse(((Double)returnValue.getValue()).doubleValue())

The XML document is then sent to the fromWS@open3soap queue, which is received by the OrderApplication.

Our final class is ShippingService, a very simple Web service:

package org.open3.soap;

public class ShippingService {

    public double calculateShippingCost(double shippingWeight, int destinationDistance) {
        
        return 5.0 + (shippingWeight * .75) + (destinationDistance * .001);
    }

}

Our example utilizes Tomcat to host the Web service. Setting up the Web service in Tomcat is outside of the scope of this article, but take a look at James Goodwill's article on " Using SOAP with Tomcat."

You can utilize SOAP's graphical admin tool to configure the ShippingService as follows:

Tomcat Configuration:
SOAP Web Administration
Deploy:
ID: urn:open3soap
Scope: Application
Methods: calculateShippingCost
Provider Type: Java
Java Provider: 
    Provider Class: org.open3.soap.ShippingService
    Static: No

Running the Software

  1. Start the Open3.org JMS server.
  2. Start Tomcat, which has been configured with Apache SOAP and the ShippingService Web service.
  3. Start the ShippingClient.
  4. Start the OrderApplication.

Note: The ShippingClient needs both the Open3.org JMS jars and Apache SOAP jars in the classpath. The OrderApplication needs only the Open3.org JMS jars.

Conclusion

This decoupled architecture of JMS and Web service clients is obviously more complex than an application that makes the Web service call directly. Certainly, for this example, the architecture is overkill. However, as the number of applications and Web services grow, the importance of the loosely coupled architecture becomes more evident. JMS forms the backbone of many integration projects for good reason: it simplifies and enhances the handling of data. Since Web services are simply another way to integrate information and functionality, it makes sense that JMS should also be a part of it.

Gunnison Carbone is an experienced Java and Web services developer with Open3 Technologies, Inc.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.