Java Message Service (JMS)

Java Message Service (JMS)

By Dean Wette, OCI Principal Software Engineer 

October 2003



Introduction

Distributed and enterprise computing presents many challenges to software architects. One fairly typical problem revolves around the fact that many of today's large scale systems integrate newer object-oriented components with legacy systems, resulting in the problem of designing and implementing reliable inter-application communication.

Numerous solutions exist for getting mutually exclusive, and otherwise incompatible, applications to collaborate via data and functional exchange. Considering them all, or even a few, is beyond the scope of this article, but enterprise messaging stands out in particular since it works well with both legacy and more modern object-oriented technologies. Messaging systems emerged to support legacy architectures like transaction process monitors, but have evolved to fit nicely within more modern software engineering technologies as well.

This article examines messaging concepts in general, why one might consider using message services, introduces the Java Message Service API v1.02 for interacting with messaging systems, and illustrates Java-based messaging with a simple example application.

Remote Procedure Call

Remote Procedure Call (RPC) is a traditional model for enabling inter-application communication between components in an enterprise system, and is still used widely with technologies such as CORBA and Microsoft DCOM, as well as Java RMI, which comprises the basis for component and distributed technologies such as Enterprise Java Beans (EJB) and JINI.

But RPC is a synchronous model. When a client application invokes a remote procedure it blocks until the procedure completes and returns to the caller. It essentially emulates in-process sequential flow control among otherwise separate and independent processes. While RPC has its place and works well in many situations, it can be problematic for large-scale distributed systems with many components. The request-response model of RPC results in tight coupling of components engaged in communication. As the number of components grows, the number of possible (many-to-many) RPC couplings grows polynomially, further increasing system complexity and maintenance costs when components are added or changed. Moreover, it makes the system more fragile, where partial failures can propagate to system-wide failure, and network and component latencies have greater impact on the performance of the system as a whole.

Messaging Systems

Messaging systems support an asynchronous, event-driven model for communication between components. Instead of establishing tightly-coupled connections between participating components, a centralized service managed by Message Oriented Middleware (MOM), handles communication using a store-and-forward mechanism for transporting autonomous messages. This yields a loosely-coupled system where components can be added and removed dynamically without modifying or otherwise impacting the existing system as a whole. In such a system the MOM manages message routing among messaging clients, which produce and/or consume messages. The messages are entirely atomic, which serves to decouple the messaging applications further. Other important aspects of messaging include MOM support for message persistence and guaranteed delivery to consumers, even after partial failures, and transactions for managing groups of messages. These features facilitate building high performance, scalable, and robust distributed systems that are highly tolerant of change.

message producer creates a message and delivers it to one of any number of destinations managed by the MOM. The MOM acknowledges receipt to the producer, which continues its normal processing without waiting for the consumer to receive the message. Thus, message production is asynchronous and the system remains loosely coupled among components, which collaborate only via the MOM. The message itself is an atomic logical unit. It contains a header, with metadata and properties set and used by the MOM and message clients, and the payload containing the actual business data intended for processing by the recipient.

message consumer specifies interest in receiving messages by registering with the MOM for asynchronous notification of message delivery via a particular destination. Depending on configuration, a consumer might receive messages even if it is offline during message production. Messaging clients can be both producers and consumers, and often are.

In cases where a request/response communication protocol is appropriate or necessary, clients may send and receive messages synchronously. In a typical scenario, a message sender produces a request message synchronously, blocking while waiting for a response. The MOM delivers the message to the consumer, which processes the message and in turn produces a message as the response. The MOM then delivers the response message to the requester to complete the cycle. In this scenario the requester is both synchronous producer and synchronous receiver. Complex systems often combine asynchronous and synchronous messaging, and MOMs generally provide support to prevent indefinite blocking of synchronous clients using timeouts and/or message expirations.

Messaging Models

Messaging systems support either or both of two messaging models, Publish and Subscribe (pub/sub) and Point To Point (p2p). The pub/sub model features one-to-many message delivery where a single message publisher publishes messages to a topic (a MOM managed destination) for delivery to any number of message subscribers. This model is typical of a push-based system for broadcasting messages, similar to an internet mailing list where messages go to all members subscribed to the list. In contrast, the p2p model specifies at-most-once delivery where a message sender sends a message to a queue (the other type of MOM-managed destination) for delivery to one specific message receiver. The p2p model is more typical in an order system where one messaging client sends a product order to another client for processing, or in the request/response scenario described above. In both models, messaging may be either asynchronous, synchronous, or a combination of the two. Applications may, and often do, make use of both messaging models, although not all MOMs support both.

The Java Message Service API

The Java Message Service (JMS) API defines a set of interfaces for interacting with MOMs. Similar to other APIs like JDBC, Servlets, and Enterprise Java Beans (EJB), JMS supports a plugable architecture for implementing vendor-neutral systems. Developers creating messaging systems program using the interfaces while vendors provide concrete implementations exposed via the JMS API. This provides the advantage of decoupling messaging clients from the underlying implementation, making systems portable across different vendors.

JMS Architecture

The JMS architecture is based primarily on provider-managed/administered objects and factory methods that return concrete JMS objects representing messaging concepts used in a Java messaging system. ConnectionFactory and Destination are the managed/administered objects discovered at runtime using using Java Naming and Directory Interface (JNDI). All others are obtained via methods defined by the JMS API. The following diagram illustrates this architecture at a high level.

JMS Architecture

JMS API

JMS supports both messaging models, pub/sub and p2p. The API, therefore, defines sub-interfaces for each of the high-level interfaces. These are the interfaces typically used in Java messaging code. The higher-level interfaces capture the general messaging concepts while the sub-interfaces expose functionality unique to their respective messaging models. They are also named after the concepts they encapsulate.

ConceptJMS Parent InterfacePublish/Subscribe ModelPoint-to-Point Model
  ConnectionFactory TopicConnectionFactory QueueConnectionFactory
  Connection TopicConnection QueueConnection
MOM destination Destination Topic Queue
  Session TopicSession QueueSession
message producer MessageProducer TopicPublisher QueueSender
message consumer MessageConsumer TopicSubscriber QueueReceiver 
QueueBrowser

Session objects are used to create message objects, of which there are several types.

TypePayload
Message No payload. Typically used for simple events.
ObjectMessage Serialized java object.
TextMessage Text, plain or XML, etc.
MapMessage Collection of key/value pairs.
ByteMessage Array of bytes.
StreamMessage Stream of Java primitive type values.

Since the JMS API defines only the abstraction of a messaging system, JNDI is used to "bootstrap" the system for a particular vendor implementation.

A typical approach for specifying JNDI lookup properties for a non-J2EE JMS application utilizes a jndi.properties file located in the application classpath. Using the open-source JMS implementation openjms (available from http://openjms.sourceforge.net/) as an example, the required properties are as follows.

java.naming.factory.initial=org.exolab.jms.jndi.InitialContextFactory
java.naming.provider.url=rmi://localhost:1099/JndiServer

Thus, the bootstrapping code used to obtain ConnectionFactory objects:

  1. import javax.jms.*;
  2. import javax.naming.*;
  3.  
  4. // ...
  5.  
  6. Context context = new InitialContext();
  7.  
  8. // pub/sub model
  9. // get a TopicConnectionFactory object from JNDI
  10. TopicConnectionFactory topicFactory =
  11. (TopicConnectionFactory)context.lookup("JmsTopicConnectionFactory");
  12. // now use the JMS API to get a TopicConnection object
  13. TopicConnection topicConnection = topicFactory.createTopicConnection();
  14.  
  15. // p2p model is similar
  16. QueueConnectionFactory queueFactory =
  17. (QueueConnectionFactory)context.lookup("JmsQueueConnectionFactory");
  18. QueueConnection queueConnection = queueFactory.createQueueConnection();
  19.  
  20. // JMS API factory methods provide other JMS objects,
  21. // except for Destination (Topic/Queue) objects obtained via JNDI
  22. // ...

JMS Example - ChatRoom

The following example illustrates some highlights of the JMS API and messaging in general. It's a simple command-line application that implements a chat room client, where a named user can engage in a chat with any number of other chat room client users. Since all messages are seen by all users in the chat room, the pub/sub model is used. If this were an instant messaging application where two users engage in private conversation, the p2p model might be a better choice. Java code for the complete application is given first, followed by discussion of the salient points.

  1. package com.ociweb.jms;
  2.  
  3. import java.io.*;
  4. import javax.jms.*;
  5. import javax.naming.*;
  6.  
  7. /**
  8.  * Simple console-based JMS chat room client.
  9.  */
  10. public class ChatRoom implements MessageListener {
  11.  
  12. private String name;
  13.  
  14. private TopicConnection connection;
  15. private TopicSession subscriberSession;
  16. private TopicSession publisherSession;
  17. private TopicPublisher publisher;
  18. private TopicSubscriber subscriber;
  19.  
  20. /**
  21.   * Creates a chat room client.
  22.   * @param name the chat room user
  23.   * @param filter ignore own messages if true
  24.   */
  25. public ChatRoom(String name, boolean filter) throws Exception {
  26. // set user's name (for chat room display)
  27. this.name = name;
  28.  
  29. // look up jms connection factory from JNDI
  30. Context context = new InitialContext();
  31. TopicConnectionFactory factory = (TopicConnectionFactory)
  32. context.lookup("JmsTopicConnectionFactory");
  33. // create connection to message server
  34. connection = factory.createTopicConnection();
  35.  
  36. // look up chat topic from JNDI
  37. Topic topic = (Topic) context.lookup("ChatTopic");
  38.  
  39.  
  40. // create chat message publisher
  41. publisherSession =
  42. connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
  43. publisher = publisherSession.createPublisher(topic);
  44.  
  45. // create chat message subscriber
  46. subscriberSession =
  47. connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
  48. String nameFilter = filter ? name : "";
  49. String selector = "name <> '" + nameFilter + "'";
  50. subscriber =
  51. subscriberSession.createSubscriber(topic, selector, false);
  52. subscriber.setMessageListener(this);
  53.  
  54. // join chat room
  55. connection.start();
  56. }
  57.  
  58. /**
  59.   * Invoked when someone posts a message to the chat room.
  60.   * @see javax.jms.MessageListener#onMessage(javax.jms.Message)
  61.   */
  62. public void onMessage(Message message) {
  63. try {
  64. TextMessage chatMessage = (TextMessage) message;
  65. String msgSender = chatMessage.getStringProperty("name");
  66. String msgBody = chatMessage.getText();
  67. System.out.println(msgSender + ": " + msgBody);
  68. } catch (JMSException e) {
  69. e.printStackTrace();
  70. }
  71. }
  72.  
  73. /**
  74.   * Publishes a message to the chat room.
  75.   * @param text the message body
  76.   * @throws JMSException
  77.   */
  78. public void postMessage(String text) throws JMSException {
  79. // create and publish message
  80. Message message = publisherSession.createTextMessage(text);
  81. message.setStringProperty("name", name);
  82. publisher.publish(message);
  83. }
  84.  
  85. /**
  86.   * Closes the JMS connection.
  87.   * @throws JMSException
  88.   */
  89. public void close() throws JMSException {
  90. if (connection != null) {
  91. connection.close();
  92. }
  93. }
  94.  
  95. public static void main(String[] args) throws Exception {
  96. if (args.length == 0) {
  97. System.out.println("Usage: ChatRoom <name> [(true|false)]");
  98. System.exit(0);
  99. }
  100.  
  101. String name = args[0];
  102. boolean filter = args.length > 1
  103. ? "true".equalsIgnoreCase(args[1])
  104. : false;
  105. ChatRoom chat = new ChatRoom(name, filter);
  106. boolean done = false;
  107. String text;
  108. BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  109. while (!done) {
  110. text = in.readLine().trim();
  111. if ("bye".equalsIgnoreCase(text)) done = true;
  112. chat.postMessage(text);
  113. }
  114. chat.close();
  115. System.exit(0);
  116. }
  117. }

Following the statements for importing the JMS and JNDI APIs, ChatRoom is defined to implement the MessageListener interface. JMS defines this interface to support asynchronous message consumption with a single method, void onMessage(Message message), and it's the same event listener pattern familiar to most Java programmers, at least those using AWT/Swing events. In this case, the event source is the MOM and the event object is the message. Unlike AWT/Swing events, JMS does not expose the event source directly via the event object, although the message headers do provide information about the client producing the message and the destination through which the message was routed.

The code to bootstrap messaging is similar to that described earlier, but following that is the statement to lookup the Topic object representing the destination for chat messages.

Topic topic = (Topic) context.lookup("ChatTopic");

The topic "ChatTopic" is a MOM-administered object. It must be obtained via a JNDI lookup, and if it doesn't exist the application will fail. MOM providers provide an administrative interface for managing destinations (creation, removal, etc.), and they vary in detail from vendor to vendor. JMS does not specify any standard for destination administration. In some cases vendors may supply a Java API for administration. In the case of openjms, a simple Java GUI manages message destinations. It is possible to create topics and queues programmatically using JMS, but they are temporary and non-persistent, with a lifetime associated with the JMS connections that create them. Temporary destinations do serve a purpose, but aren't particularly useful to the ChatRoom application.

Sessions provide the context for producing and consuming messages. Since this client is both producer and consumer, it needs two Session objects, one for each role, from which TopicPublisher and TopicSubscriber objects are obtained for sending and receiving messages.

  1. // create chat message publisher
  2. publisherSession =
  3. connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
  4. publisher = publisherSession.createPublisher(topic);
  5.  
  6. // create chat message subscriber
  7. subscriberSession =
  8. connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
  9. String nameFilter = filter ? name : "";
  10. String selector = "name <> '" + nameFilter + "'";
  11. subscriber =
  12. subscriberSession.createSubscriber(topic, selector, false);
  13. subscriber.setMessageListener(this);

The selector string specifies a message filter. JMS provides a message selector syntax for filtering messages in cases where a client may not be interested in all messages published to a destination. In this case, users can elect not to receive messages they publish. The statement subscriber.setMessageListener(this) registers this client as an asynchronous message consumer interested in "ChatTopic" messages.

With initialization completed, start the connection to the MOM, and the client is ready to consume and publish chat messages. Since connections consume system resources they should always be closed when no longer needed. The helper method close() handles this when the ChatRoom client exits.

When the MOM delivers messages from the topic to asynchronous consumers, it invokes the onMessage method, described above, with the message argument. The message itself carries all the information necessary to carry out the business logic, and includes information about the sender, and possibly application-specific properties set in the message header by the sender. In this case the name header property identifies the name of the client producing this message.

  1. public void onMessage(Message message) {
  2. try {
  3. TextMessage chatMessage = (TextMessage) message;
  4. String msgSender = chatMessage.getStringProperty("name");
  5. String msgBody = chatMessage.getText();
  6. System.out.println(msgSender + ": " + msgBody);
  7. } catch (JMSException e) {
  8. e.printStackTrace();
  9. }
  10. }

To send a message, a client creates a message object using one of the message factory methods in Session, populates it with data, and publishes it to the MOM. ChatRoom clients also set the name header property to identify themselves to other clients receiving the message.

  1. public void postMessage(String text) throws JMSException {
  2. // create and publish message
  3. Message message = publisherSession.createTextMessage(text);
  4. message.setStringProperty("name", name);
  5. publisher.publish(message);
  6. }

Lastly, the main method provides the code to manage the client lifecycle. It's invoked with arguments for the client name and to specify whether self-sent messages are filtered. Once the ChatRoom constructor is invoked the client starts consuming messages as they are published. The while loop provides the mechanism for publishing messages, until the user types "bye" to exit.

  1. public static void main(String[] args) throws Exception {
  2. // ...
  3. String name = args[0];
  4. boolean filter = args.length > 1
  5. ? "true".equalsIgnoreCase(args[1])
  6. : false;
  7. ChatRoom chat = new ChatRoom(name, filter);
  8. boolean done = false;
  9. String text;
  10. BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  11. while (!done) {
  12. text = in.readLine().trim();
  13. if ("bye".equalsIgnoreCase(text)) done = true;
  14. chat.postMessage(text);
  15. }
  16. chat.close();
  17. System.exit(0);
  18. }

Running the ChatRoom requires starting a MOM and two or more ChatRoom clients (since running a single client gets dull very quickly). The mechanism for starting a MOM is vendor dependent. With openjms one simply invokes a shell script.

JMS and J2EE

The Java Message Service API comprises part of the J2EE specification. Most notable is support in J2EE for asynchronous messaging with MessageDrivenBeans (MDBs), a new type of EJB bean as of the EJB 2.0 specification. Other than MDBs, only application clients may be asynchronous message consumers within J2EE, although other components may be asynchronous message producers and synchronous consumers. But further exploration of JMS' role in J2EE is left for future discussion. The J2EE 1.4 specification, still under review as of this writing, defines support for JMS 1.1, which has several improvements and simplifies using the API (this article covers JMS 1.02).

Summary

Surprisingly, JMS seems to be utilized far less than it might be. It's powerful, easy to use, and supported by major industry vendors of enterprise technologies. Implementations of JMS from IBM, BEA, HP, Oracle, Sun, and other companies are readily available, and useful both in pure Java/J2EE applications or integrated with legacy systems. While JMS won't, and shouldn't, replace RPC entirely, it is more appropriate to many situations where RPC is used. I expect JMS to find a larger role once integrators realize its advantages and learn to understand messaging better.

References



Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret