Chapter 2. Understanding message-oriented middleware and JMS

 

This chapter covers

  • Enterprise messaging and message-oriented middleware
  • Understanding the Java Message Service (JMS)
  • Using the JMS APIs for sending and receiving messages
  • An example of a message-driven bean

 

To help you better understand the ideas behind ActiveMQ, it’s important to have some background and history on enterprise messaging in general. After discussing enterprise messaging, you’ll be prepared for a brief introduction to JMS followed by some small examples of its use. The purpose of this chapter is to briefly review enterprise messaging and the JMS specification. If you’re already familiar with these topics, you can skip ahead to the next chapter.

At one time or another, every software developer needs to communicate between applications or transfer data from one system to another. Not only are there various solutions to this sort of problem, but depending on your constraints and requirements, deciding how to go about such a task can be a big decision. Business requirements often place restrictions on items that directly impact such a decision including performance, scalability, reliability, and more. There are numerous applications that we use every day that impose such requirements including ATMs, airline reservation systems, credit card systems, point-of-sale systems, and telecommunications, to name a few. Where would we be without most of these applications in our daily lives?

For a moment, think about how these types of services have made your life easier. These applications and others like them are made possible because of their reliable and secure nature. Behind the scenes of these applications, just about all of them are composed of many applications, usually distributed, communicating by passing events or messages back and forth. Even the most sophisticated financial trading systems are integrated in this manner, operating completely through the sending and receipt of business information among all the necessary systems using messaging.

Many products provide messaging for various purposes. Necessity is the mother of invention, and this is how messaging middleware was born. A form of software became necessary for communication and data transfer capabilities that could more easily manage the disparity among data formats, operating systems, protocols, and even programming languages. Additionally, capabilities such as sophisticated message routing and transformation began to emerge as part of or in conjunction with these solutions. Such systems came to be known as message-oriented middleware (MOM).

ActiveMQ is a MOM product that provides asynchronous messaging for such business systems. By providing a MOM that utilizes the JMS spec, ActiveMQ facilitates application architectures that support such reliability and scalability.

2.1. Introduction to enterprise messaging

Most systems like those mentioned previously were built using mainframe computers and many still use them today. So how can these applications work in such a reliable manner? To answer this and other questions, let’s briefly explore some of the history behind such solutions and how enterprise messaging was born.

Starting in the 1960s, large organizations invested in mainframes for critical applications to facilitate functions such as data processing, financial processing, statistical analysis, and much more. Mainframes provided appreciable benefits including high availability, redundancy, reliability and scalability, upgradability without service interruption, and many other critical features required by business. Although these systems were extremely powerful, access to such systems was restricted, as input options were few. Also, interconnectivity among systems hadn’t yet been invented, meaning that parallel processing wasn’t yet possible.

Figure 2.1 shows a diagram demonstrating how terminals connect to a mainframe. In the 1970s, users began to access mainframes through terminals, which dramatically expanded the use of these systems by allowing thousands of concurrent users. It was during this period that computer networks were invented and connectivity among mainframes themselves now became possible. By the 1980s, not only were graphical terminals available, but PCs were also invented and terminal emulation software quickly became common. Interconnectivity became even more important because applications needing access to the mainframe were being developed to run on PCs and workstations. Figure 2.2 shows these various types of connectivity to the mainframe. Note how this expanded connectivity introduced additional platforms and protocols, posing a new set of problems to be addressed.

Figure 2.1. Standalone terminals connecting to a mainframe using a single protocol

Figure 2.2. Standalone terminals and applications connecting to a mainframe using many protocols.

Connecting a source system and a target system wasn’t easy since each data format, each piece of hardware, and each protocol required a different type of adapter. As the list of adapters grew, so did the versions of each, causing them to become difficult to maintain. Soon the effort required to maintain the adapters outweighed that of the systems themselves. This is where enterprise messaging entered the picture.

The purpose of enterprise messaging was to transfer data among disparate systems by sending messages from one system to another. There have been numerous technologies for various forms of messaging through the years, including the following list:

  • Solutions for remote procedure calls (RPC) such as COM, CORBA, DCE, and EJB
  • Solutions for event notification, inter-process communication, and message queuing that are baked into operating systems such as FIFO buffers, message queues, pipes, signals, sockets, and others
  • Solutions for a category of middleware that provides asynchronous, reliable message queuing such as IBM WebSphere MQ, SonicMQ, TIBCO Rendezvous, and Apache ActiveMQ, commonly used for Enterprise Application Integration (EAI) purposes

The last category of messaging middleware products is what we’ll discuss here. So what exactly is message-oriented middleware?

2.2. What’s message-oriented middleware?

Message-oriented middleware (MOM) is best described as a category of software for communication in an asynchronous, loosely-coupled, reliable, scalable, and secure manner among distributed applications or systems. MOMs were an important concept in the distributed computing world. They allowed application-to-application communication using APIs provided by each vendor, and began to deal with many issues in the distributed system space.

The overall idea behind a MOM is that it acts as a message mediator between message senders and message receivers. This mediation provides a whole new level of loose coupling. Figure 2.3 demonstrates how a MOM is used to mediate connectivity and messaging not only between each application and the mainframe but also from application to application.

Figure 2.3. Message-oriented middleware mediates messages to the mainframe and guarantees message delivery.

At a high level, messages are a unit of business information that’s sent from one application to another via the MOM. Applications send and receive messages via the MOM using what are known as destinations. Messages are addressed to and delivered to receivers that connect or subscribe to the destinations. This is the mechanism that allows for loose coupling between senders and receivers, as there’s no requirement for each to be connected to the MOM at the same time for sending and receiving messages. Senders know nothing about receivers and receivers know nothing about senders. This is known as asynchronous messaging.

MOMs added welcome additional features to enterprise messaging that weren’t previously possible when systems were tightly coupled—features such as message persistence, robust communication over slow or unreliable connections, complex message routing, message transformation, and much more. Message persistence helps to mitigate slow or unreliable connections made by senders and receivers; or in a situation where a receiver simply fails, it won’t affect the state of the sender. Complex message routing opens up a huge number of possibilities, including delivering a single message to many receivers, message routing based on properties or the content of a message, and so forth. Message transformation allows two applications that don’t handle the same message format to now communicate via a custom message format that’s transformed on the fly.

Additionally, many MOMs on the market today provide support for a diverse set of protocols for connectivity. Some commonly supported protocols include HTTP/S, multicast, SSL, TCP/IP, UDP, and more. Some vendors even provide support for multiple languages, further lowering the barrier to using MOMs in a wide variety of environments. ActiveMQ provides exactly these types of features and more.

Furthermore, it’s typical for a MOM to provide an API for sending and receiving messages and otherwise interacting with the MOM. For years, all MOM vendors provided their own proprietary APIs for whatever languages they chose. That is, until the Java Message Service (JMS) came along.

2.3. What’s the Java Message Service?

The Java Message Service (JMS) moved beyond vender-centric MOM APIs to provide an API for enterprise messaging. JMS aims to provide a standardized API to send and receive messages using the Java programming language in a vendor-neutral manner. The JMS API minimizes the amount of enterprise messaging knowledge a Java programmer is required to possess in order to develop complex messaging applications, while still maintaining a certain amount of portability across JMS provider implementations.

JMS isn’t itself a MOM. It’s an API that abstracts the interaction between messaging clients and MOMs in the same manner that JDBC abstracts communication with relational databases. Figure 2.4 shows at a high level how JMS provides an API used by messaging clients to interact with MOM-specific JMS providers, which handle interaction with the vendor-specific MOM. The JMS API lowers the barrier to creating enterprise messaging applications. It also eases the portability to other JMS providers.

Figure 2.4. JMS allows a single client to easily connect to many JMS providers.

Originally created by Sun in conjunction with a group of companies from the enterprise messaging industry, the first version of the JMS spec was released in 1998. The latest release was in 2002 and offered some necessary improvements. The JMS 1.1 release unified the two sets of APIs for working with the two messaging domains, so working with both messaging domains now only requires a single common API. This was a dramatic change that improved the APIs. Backward compatibility with the old APIs is still supported.

In standardizing the API, JMS formally defined many concepts and artifacts from the world of messaging:

  • JMS client— An application is written using 100% pure Java to send and receive messages.
  • Non-JMS client— An application is written using the JMS provider’s native client API to send and receive messages instead of JMS.
  • JMS producer— A client application that creates and sends JMS messages.
  • JMS consumer— A client application that receives and processes JMS messages.
  • JMS provider— The implementation of the JMS interfaces, which is ideally written in 100% pure Java.
  • JMS message— The most fundamental concept of JMS; sent and received by JMS clients.
  • JMS domains— The two styles of messaging that include point-to-point and publish/subscribe.
  • Administered objects— Preconfigured JMS objects that contain provider-specific configuration data for use by clients. These objects are typically accessible by clients via JNDI.
  • Connection factory— Clients use a connection factory to create connections to the JMS provider.
  • Destination— An object to which messages are addressed and sent and from which messages are received.

Besides these concepts, others are also important. The next few sections will dive deeper into these concepts and focus on describing these building blocks of JMS.

2.4. The JMS specification

As mentioned in the previous section, the JMS spec defines two types of clients—JMS clients and non-JMS clients. The differences are worth a brief discussion, so let’s address them.

2.4.1. JMS clients

JMS clients utilize the JMS API for interacting with the JMS provider. Similar in concept to using the JDBC API to access data in relational databases, JMS clients use the JMS API for standardized access to the messaging service. Many JMS providers (including ActiveMQ) include features beyond those required by JMS. It’s worth noting that a 100% pure JMS client would only use the JMS APIs and would avoid using such additional features. But the choice to use a particular JMS provider is often driven by the additional features offered. If a JMS client uses such additional features, this client may not be portable to another JMS provider without a refactoring effort.

JMS clients utilize the MessageProducer and MessageConsumer interfaces in some way. It’s the responsibility of the JMS provider to furnish an implementation of each of these interfaces. A JMS client that sends messages is known as a producer and a JMS client that receives messages is known as a consumer. It’s possible for a JMS client to handle both the sending and receiving of messages.

JMS Producers

JMS clients use the JMS MessageProducer class for sending messages to a destination. The default destination for a given producer is set when the producer is created using the Session.createProducer() method. But this can be overridden for individual messages by using the MessageProducer.send() method. The MessageProducer interface is shown here.

Listing 2.1. The MessageProducer interface
public interface MessageProducer {
    void setDisableMessageID(boolean value) throws JMSException;

    boolean getDisableMessageID() throws JMSException;

    void setDisableMessageTimestamp(boolean value) throws JMSException;

    boolean getDisableMessageTimestamp() throws JMSException;

    void setDeliveryMode(int deliveryMode) throws JMSException;

    int getDeliveryMode() throws JMSException;

    void setPriority(int defaultPriority) throws JMSException;

    int getPriority() throws JMSException;

    void setTimeToLive(long timeToLive) throws JMSException;

    long getTimeToLive() throws JMSException;
    Destination getDestination() throws JMSException;

    void close() throws JMSException;

    void send(Message message) throws JMSException;

    void send(Message message, int deliveryMode, int priority,
              long timeToLive)
        throws JMSException;

    void send(Destination destination, Message message)
        throws JMSException;

    void send(
        Destination destination,
        Message message,
        int deliveryMode,
        int priority,
        long timeToLive)
        throws JMSException;
}

The MessageProducer provides methods not only for sending messages but also for setting various message headers including the JMSDeliveryMode, the JMSPriority, the JMSExpiration (via the get/setTimeToLive() method), as well as a utility send() method for setting all three of these at once. These message headers are discussed in section 2.4.5.

JMS Consumers

JMS clients use the JMS MessageConsumer class for consuming messages from a destination. The MessageConsumer can consume messages either synchronously by using one of the receive() methods or asynchronously by providing a MessageListener implementation to the consumer. The MessageListener.onMessage() method is invoked as messages arrive on the destination. The MessageConsumer interface is shown next.

Listing 2.2. The JMS MessageConsumer interface
public interface MessageConsumer {
    String getMessageSelector() throws JMSException;

    MessageListener getMessageListener() throws JMSException;

    void setMessageListener(MessageListener listener) throws JMSException;

    Message receive() throws JMSException;

    Message receive(long timeout) throws JMSException;

    Message receiveNoWait() throws JMSException;

    void close() throws JMSException;
}

There’s no method for setting the destination on the MessageConsumer. Instead the destination is set when the consumer is created using the Session.createConsumer() method.

2.4.2. Non-JMS clients

As noted earlier, a non-JMS client uses a JMS provider’s native client API instead of the JMS API. This is an important distinction because native client APIs might offer some different features than the JMS API. Such non-JMS APIs could consist of utilizing the CORBA IIOP protocol or some other native protocol beyond Java RMI. Messaging providers that predate the JMS spec commonly have a native client API, but many JMS providers also provide a non-JMS client API.

2.4.3. The JMS provider

The JMS provider is the vendor-specific MOM that implements the JMS API. Such an implementation provides access to the MOM via the standardized JMS API (remember the analogy to JDBC).

2.4.4. The JMS message

The JMS message is the most important concept in the JMS specification. Every concept in the JMS spec is built around handling a JMS message because it’s how business data and events are transmitted. A JMS message allows anything to be sent as part of the message, including text and binary data as well as information in the headers. As depicted in figure 2.5, JMS messages contain two parts, including headers and a pay-load. The headers provide metadata about the message used by both clients and JMS providers. The payload is the actual body of the message and can hold both textual and binary data via the various message types.

Figure 2.5. A graphical representation of a JMS message

The JMS message is designed to be easy to understand and flexible. All the complexity of the JMS message resides in the headers.

2.4.5. JMS message internals

As mentioned previously, the complexity of a JMS message lies in the details provided by the headers. There are actually two types of headers, which are basically the same logical concept but differ semantically. Whereas a standard list of headers and methods to work with them are provided by the JMS spec, properties are designed to facilitate custom headers based on primitive Java types. Both are referred to generically as headers.

JMS Message Headers

As shown in figure 2.5, JMS messages support a standard lists of headers and the JMS API provides methods for working with them. Many of the headers are automatically assigned. The following list describes each of these headers, and how they are assigned to the message.

Headers set automatically by the client’s send() method:

  • JMSDestination— The destination to which the message is being sent. This is valuable for clients who consume messages from more than one destination.
  • JMSDeliveryMode— JMS supports two types of delivery modes for messages: persistent and nonpersistent. The default delivery mode is persistent. Each delivery mode incurs its own overhead and implies a particular level of reliability.

    • Persistent— Advises the JMS provider to persist the message so it’s not lost if the provider fails. A JMS provider must deliver a persistent message once and only once. In other words, if the JMS provider fails, the message won’t be lost and won’t be delivered more than once. Persistent messages incur more overhead due to the need to store the message, and value reliability over performance.
    • Nonpersistent— Instructs the JMS provider not to persist the message. A JMS provider must deliver a nonpersistent message at most once. In other words, if the JMS provider fails, the message may be lost, but it won’t be delivered twice. Nonpersistent messages incur less overhead and value performance over reliability.
    The delivery mode is set on the producer and is applied to all messages sent from that producer. But the delivery mode can be overridden for individual messages.
  • JMSExpiration— The time that a message will expire. This header is used to prevent delivery of a message after it has expired. The expiration value for messages can be set using either the MessageProducer.setTimeToLive() method to set the time-to-live globally for all messages sent from that producer, or using one of the MessageProducer.send() methods to set the time-to-live locally for each message that is sent. Calling any of these methods sets the default length of time in milliseconds that a message should be considered usable, although the MessageProducer.send() methods take precedence. The JMSExpiration message header is calculated by adding the time-to-live to the current time in GMT. By default the time-to-live is zero, meaning that the message won’t expire. If a time-to-live isn’t specified, the default value is used and the message won’t expire. If the time-to-live is explicitly specified as zero, then the same is true and the message will not expire. This header can be valuable for time-sensitive messages. But be aware that JMS providers shouldn’t deliver messages that have expired, and JMS clients should be written so as to not process messages that have expired.
  • JMSMessageID— A string that uniquely identifies a message that’s assigned by the JMS provider and must begin with ID. The message ID can be used for message processing or for historical purposes in a message storage mechanism. Because message IDs can cause the JMS provider to incur some overhead, the producer can advise the JMS provider that the JMS application doesn’t depend on the value of this header via the MessageProducer.setDisableMessageID() method. If the JMS provider accepts this advice, the message ID must be set to null. Be aware that a JMS provider may ignore this call and assign a message ID anyway.
  • JMSPriority— Used to assign a level of importance to a message. This header is also set on the message producer. Once the priority is set on a producer, it applies to all messages sent from that producer. The priority can be overridden for individual messages. JMS defines 10 levels of message priority, ranging from 0 (the lowest) to 9 (the highest):

    • Priorities 0–4— These priorities are finer granularities of the normal priority.
    • Priorities 5–9— These priorities are finer granularities of expedited priority.
    JMS providers aren’t required to implement message ordering, although most do. They should simply attempt to deliver higher-priority messages before lower-priority messages.
  • JMSTimestamp— This header denotes the time the message was sent by the producer to the JMS provider. The value of this header uses the standard Java millis time value. Similar to the JMSMessageID header, the producer may advise the JMS provider that the JMSTimestamp header isn’t needed via the Message-Producer.setDisableMessageTimestamp() method. If the JMS provider accepts this advice, it must set the JMSTimestamp to zero.

Header set optionally by the client:

  • JMSCorrelationID— Used to associate the current message with a previous message. This header is commonly used to associate a response message with a request message. The value of the JMSCorrelationID can be one of the following:

    • A provider-specific message ID
    • An application-specific String
    • A provider-native byte [] value
    The provider-specific message ID will begin with the ID: prefix, whereas the application-specific String must not start with the ID: prefix. If a JMS provider supports the concept of a native correlation ID, a JMS client may need to assign a specific JMSCorrelationID value to match that expected by non-JMS clients, but this isn’t a requirement.
  • JMSReplyTo— Used to specify a destination where a reply should be sent. This header is commonly used for request/reply style of messaging. Messages sent with this header populated typically expect a response, but it’s actually optional. The client must make the decision to respond or not.
  • JMSType— Used to semantically identify the message type. This header is used by few vendors and has nothing to do with the payload Java type of the message.

Headers set optionally by the JMS provider:

  • JMSRedelivered— Used to indicate the liklihood that a message was previously delivered but not acknowledged. This can happen if a consumer fails to acknowledge delivery, or if the JMS provider hasn’t been notified of delivery such as an exception being thrown that prevents the acknowledgement from reaching the provider.
JMS Message Properties

Properties are simply additional headers that can be specified on a message. JMS provides the ability to set custom headers using generic methods. Methods are provided for working with many primitive Java types for header values including Boolean, byte, short, int, long, float, double, and also the String object type. Examples of these methods can be seen in the next listing, taken from the Message interface.

Listing 2.3. The JMS Message interface
public interface Message {
...
    boolean getBooleanProperty(String name) throws JMSException;
    byte getByteProperty(String name) throws JMSException;
    short getShortProperty(String name) throws JMSException;
    int getIntProperty(String name) throws JMSException;
    long getLongProperty(String name) throws JMSException;
    float getFloatProperty(String name) throws JMSException;
    double getDoubleProperty(String name) throws JMSException;
    String getStringProperty(String name) throws JMSException;
    Object getObjectProperty(String name) throws JMSException;
...
    Enumeration getPropertyNames() throws JMSException;
    boolean propertyExists(String name) throws JMSException;
...
    void setBooleanProperty(String name, boolean value) throws JMSException;
    void setByteProperty(String name, byte value) throws JMSException;
    void setShortProperty(String name, short value) throws JMSException;
    void setIntProperty(String name, int value) throws JMSException;
    void setLongProperty(String name, long value) throws JMSException;
    void setFloatProperty(String name, float value) throws JMSException;
    void setDoubleProperty(String name, double value) throws JMSException;
    void setStringProperty(String name, String value) throws JMSException;
    void setObjectProperty(String name, Object value) throws JMSException;
....
}

Also note the two convenience methods for working with generic properties on a message—the getPropertyNames() method and the propertyExists() method. The getPropertyNames() method returns an Enumeration of all the properties on a given message to easily iterate through all of them. The propertyExists() method is for testing whether a given property exists on a message. Note that the JMS-specific headers aren’t considered generic properties and aren’t included in the Enumeration returned by the getPropertyNames() method.

There are three types of properties: custom properties, JMS defined properties, and provider-specific properties.

Custom Properties

Custom properties are arbitrary and are defined by a JMS application. Developers of JMS applications can freely define any properties using any Java types necessary, by using the generic methods shown in the previous section (getBooleanProperty()/setBooleanProperty(), getStringProperty()/setStringProperty(), and so on).

JMS-Defined Properties

The JMS spec reserves the JMSX property name prefix for JMS-defined properties, and support for these properties is optional:

  • JMSXAppID— Identifies the application sending the message
  • JMSXConsumerTXID— The transaction identifier for the transaction within which this message was consumed
  • JMSXDeliveryCount— The number of message delivery attempts
  • JMSXGroupID— The message group of which this message is a part
  • JMSXGroupSeq— The sequence number of this message within the group
  • JMSXProducerTXID— The transaction identifier for the transaction within which this message was produced
  • JMSXRcvTimestamp— The time the JMS provider delivered the message to the consumer
  • JMSXState— Used to define a provider-specific state
  • JMSXUserID— Identifies the user sending the message

The only recommendation provided by the spec for use of these properties is for the JMSXGroupID and JMSXGroupSeq properties, and that these properties should be used by clients when grouping messages and/or grouping messages in a particular order.

Provider-Specific Properties

The JMS spec reserves the JMS_<vendor-name> property name prefix for provider-specific properties. Each provider defines its own value for the <vendor-name> placeholder. These are most typically used for provider-specific non-JMS clients and shouldn’t be used for JMS-to-JMS messaging.

Now that JMS headers and properties on messages have been discussed, for what exactly are they used? Headers and properties are important when it comes to filtering the messages received by a client subscribed to a destination.

2.4.6. Message selectors

There are times when a JMS client is subscribed to a given destination, but it may want to filter the types of messages it receives. This is exactly where headers and properties can be used. For example, if a consumer registered to receive messages from a queue is only interested in messages about a particular stock symbol, this is easy as long as each message contains a property that identifies the stock symbol of interest. The JMS client can utilize JMS message selectors to tell the JMS provider that it only wants to receive messages containing a particular value in a particular property.

Message selectors allow a JMS client to specify which messages it wants to receive from a destination based on values in message headers. Selectors are conditional expressions defined using a subset of SQL92. Using Boolean logic, message selectors use message headers and properties as criteria for simple Boolean evaluation. Messages not matching these expressions aren’t delivered to the client. Message selectors can’t reference a message payload, only the message headers and properties.

Selectors use conditional expressions for selectors that are passed as String arguments using some of the creation methods in the javax.jms.Session object. The syntax of these expressions uses various identifiers, literals, and operators taken directly from the SQL92 syntax and are defined in table 2.1.

Table 2.1. JMS selector syntax

Item

Values

Literals Booleans TRUE/FALSE; numbers such as 5, -10, +34; numbers with decimal or scientific notation such as 43.3E7, +10.5239
Identifiers A header or property field
Operators AND, OR, LIKE, BETWEEN, =, <>, <, >, <=, =>, +, -, *, /, IS NULL, IS NOT NULL

The items shown in table 2.1 are used to create queries against message headers and properties. Consider the message defined in the next listing. This message defines two properties that will be used for filtering messages in the example that follows.

Listing 2.4. A JMS message with custom properties

Now let’s look at some examples of filtering messages via message selectors using the preceding message.

Listing 2.5. Filter messages using the SYMBOL header
...
String selector = "SYMBOL = 'AAPL'";

MessageConsumer consumer =
    session.createConsumer(destination, selector);
...

Listing 2.5 defines a selector to match messages for Apple, Inc. This consumer receives only messages matching the query defined in the selector.

Listing 2.6. Filter messages using both the SYMBOL and PRICE headers
...
String selector = "SYMBOL = 'AAPL' AND PRICE > "
    + getPreviousPrice();

MessageConsumer consumer =
    session.createConsumer(destination, selector);
...

This example specifies a selector to match messages for Apple, Inc. whose price is greater than the previous price. This selector will show stock messages whose price is increasing. But what if you want to take into account the timeliness of stock messages in addition to the price and symbol? Consider the next example.

Listing 2.7. Filter messages using headers
...
String selector = "SYMBOL IN ('AAPL', 'CSCO') AND PRICE > "
        + getPreviousPrice() + " AND PE_RATIO < "
        + getCurrentAcceptedPriceToEarningsRatioThreshold();

MessageConsumer consumer =
    session.createConsumer(destination, selector);
...

The last example of message selectors in listing 2.7 defines a more complex selector to match messages for Apple, Inc., and Cisco Systems, Inc., whose price is increasing and whose price-to-earnings ratio is less than the currently accepted threshold.

These examples should be enough for you to begin using message selectors. But if you want more in-depth information, see the Javadoc for the JMS Message type.

Message Body

JMS defines six Java types for the message body, also known as the payload. Through the use of these objects, data and information can be sent via the message payload.

  • Message— The base message type. Used to send a message with no payload, only headers and properties. Typically used for simple event notification.
  • TextMessage— A message whose payload is a String. Commonly used to send simple textual and XML data.
  • MapMessage— Uses a set of name/value pairs as its payload. The names are of type String and the values are a Java primitive type.
  • BytesMessage— Used to contain an array of uninterpreted bytes as the payload.
  • StreamMessage— A message with a payload containing a stream of primitive Java types that’s filled and read sequentially.
  • ObjectMessage— Used to hold a serializable Java object as its payload. Usually used for complex Java objects. Also supports Java collections.

2.4.7. JMS domains

As noted earlier, the creation of JMS was a group effort, and the group contained vendors of messaging implementations. It was the influence of existing messaging implementations that resulted in JMS identifying two styles of messaging (or domains as they’re referred to in the spec)—point-to-point and publish/subscribe. Most MOMs already supported both of these messaging styles, so it only made sense that the JMS API support both. Let’s examine each of these messaging styles to better understand them.

The Point-to-Point Domain

The point-to-point (PTP) messaging domain uses destinations known as queues. Through the use of queues, messages are sent and received either synchronously or asynchronously. Each message received on the queue is delivered once and only once to a single consumer. This is similar to a person-to-person email sent through a mail server. Consumers receive messages from the queue either synchronously using the MessageConsumer.receive() method or asynchronously by registering a MessageListener implementation using the MessageConsumer.setMessage-Listener() method. The queue stores all messages until they’re delivered or until they expire.

Multiple consumers can be registered on a single queue as shown in figure 2.6, but only one consumer will receive a given message and then it’s up to that consumer to acknowledge the message. Note that the message in figure 2.6 is sent from a single producer and is delivered to a single consumer, not all consumers. As mentioned earlier, the JMS provider guarantees the delivery of a message once and only once to the next available registered consumer. In this regard, the JMS provider is distributing the messages in a sort of round-robin style across all the registered consumers.

Figure 2.6. Point-to-point messaging uses a one-to-one messaging paradigm.

The Publish/Subscribe Domain

The publish/subscribe (pub/sub) messaging domain uses destinations known as topics. Publishers send messages to the topic and subscribers register to receive messages from the topic. Any messages sent to the topic are automatically delivered to all subscribers. This messaging domain is similar to subscribing to a mailing list where all subscribers will receive all messages sent to the mailing list in a one-to-many paradigm. The pub/sub domain is depicted in figure 2.7.

Figure 2.7. Publish/subscribe uses a one-to-many messaging paradigm.

Much the same as PTP messaging in the previous section, subscribers register to receive messages from the topic either synchronously using the Message-Consumer.receive() method or asynchronously by registering a MessageListener implementation using the MessageConsumer.setMessageListener() method. Topics don’t hold messages unless explicitly instructed to do so. This can be achieved via the use of a durable subscription. Using a durable subscription, when a subscriber disconnects from the JMS provider, it’s the responsibility of the JMS provider to store messages for the subscriber. Upon reconnecting, the durable subscriber will receive all unexpired messages from the JMS provider. Durable subscriptions allow for subscriber disconnection without missing any messages.

Distinguishing Message Durability From Message Persistence

Two points within JMS that are often confused are message durability and message persistence. Though they’re similar, there are some semantic differences between them and each has its specific purpose. Message durability can only be achieved with the pub/sub domain. When clients connect to a topic, they can do so using a durable or a nondurable subscription. Consider the differences between the two:

  • Durable subscription— A durable subscription is infinite. It’s registered with the topic subscription to tell the JMS provider to preserve the subscription state in the event that the subscriber disconnects. If a durable subscriber disconnects, the JMS provider will hold all messages until that subscriber connects again or until the subscriber explicitly unsubscribes from the topic.
  • Nondurable subscription— A nondurable subscription is finite. It’s registered with the topic subscription to tell the JMS provider to not preserve the subscription state in the event that the subscriber disconnects. If a subscriber disconnects, the JMS provider won’t hold any messages during the disconnection period.

Message persistence is independent of the message domain. Message persistence is a quality of service property used to indicate the JMS application’s ability to handle missing messages in the event of a JMS provider failure. As discussed previously, this quality of service is specified on the message producer’s setDeliveryMode method using one of the JMSDeliveryMode class’s PERSISTENT or NON-PERSISTENT properties as an argument.

 

Request/reply messaging in JMS applications

Although the JMS spec doesn’t define request/reply messaging as a formal messaging domain, it does provide some message headers and a couple of convenience classes for handling basic request/reply messaging. Request/reply messaging is an asynchronous back-and-forth conversational pattern utilizing either the PTP domain or the pub/sub domain through a combination of the JMSReplyTo and JMSCorrelationID message headers and temporary destinations. The JMSReplyTo specifies the destination where a reply should be sent, and the JMSCorrelationID in the reply message specifies the JMSMessageID of the request message. These headers are used to link the reply message(s) to the original request message. Temporary destinations are those that are created only for the duration of a connection and can only be consumed from by the connection that created them. These restrictions make temporary destinations useful for request/reply.

The convenience classes for handling basic request/reply are the QueueRequestor and the TopicRequestor. These classes provide a request() method that sends a request message and waits for a reply message through the creation of a temporary destination where only one reply per request is expected. These classes are useful only for this most basic form of request/reply, as shown in figure 2.8—one reply per request.

Figure 2.8. The steps involved in basic request/reply messaging

Figure 2.8 depicts the basic request/reply style of messaging between two endpoints. This is commonly achieved using the JMSReplyTo message header and a temporary queue where the reply message is sent by the receiver and consumed by the requestor. As stated previously, the QueueRequestor and the TopicRequestor can handle basic request/reply but aren’t designed to handle more complex cases of request/reply, such as a single request and multiple replies from many receivers. Such a sophisticated use case requires you to develop a custom JMS application.

 

2.4.8. Administered objects

Administered objects contain provider-specific JMS configuration information and are supposed to be created by a JMS administrator; hence, the name. Administered objects are used by JMS clients. They’re used to hide provider-specific details from the clients and to abstract the JMS provider’s administration tasks. It’s common to look up administered objects via JNDI, but not required. This is most common when the JMS provider is hosted in a Java EE container. The JMS spec defines two types of administered objects: ConnectionFactory and Destination.

Connectionfactory

JMS clients use the ConnectionFactory object to create connections to a JMS provider. Connections typically represent an open TCP socket between a client and the JMS provider, so the overhead for a connection is large. It’s a good idea to use an implementation that pools connections if possible. A connection to a JMS provider is similar to a JDBC connection to a relational database, in that it’s used by clients to interact with the database. JMS connections are used by JMS clients to create javax.jms.Session objects that represent an interaction with the JMS provider.

Destination

The Destination object encapsulates the provider-specific address to which messages are sent and from which messages are consumed. Although destinations are created using the Session object, their lifetime matches the connection from which the session was created.

Temporary destinations are unique to the connection that was used to create them. They’ll only live as long as the connection that created them and only the connection that created them can create consumers for them. As mentioned previously, temporary destinations are commonly used for request/reply messaging.

2.5. Using the JMS APIs to create JMS applications

JMS applications can be as simple or as complex as necessary to suit the business requirements. Just as with other APIs such as JDBC, JNDI, EJBs, and so on, it’s common to abstract the use of JMS APIs so as to not intermingle the JMS code with the business logic. This concept won’t be demonstrated here, as this is a much lengthier exercise involving patterns and full application infrastructure. Here the simplest example will be demonstrated to show a minimalist use of the JMS APIs.

2.5.1. A simple JMS application

A JMS application is written using the Java programming language and composed of many parts for handling different aspects of working with JMS. These parts were identified earlier in the chapter via the list of JMS artifacts in section 2.3. A simple JMS application will utilize the following steps:

1.  Acquire a JMS connection factory

2.  Create a JMS connection using the connection factory

3.  Start the JMS connection

4.  Create a JMS session from the connection

5.  Acquire a JMS destination

6.  Create a JMS producer, OR

a.  Create a JMS producer

b.  Create a JMS message and address it to a destination

7.  Create a JMS consumer

a.  Create a JMS consumer

b.  Optionally register a JMS message listener

8.  Send or receive JMS message(s)

9.  Close all JMS resources (connection, session, producer, consumer, and so forth)

These steps are meant to be abstract in order to demonstrate the overall simplicity of working with JMS. Using a minimal amount of code, the following listing demonstrates the steps for creating a JMS producer to send a message.

Listing 2.8. Sending a JMS message
public class MyMessageProducer {
...
  ConnectionFactory connectionFactory;
  Connection connection;
  Session session;
  Destination destination;
  MessageProducer producer;
  Message message;
  boolean useTransaction = false;
  try {
    Context ctx = new InitialContext();
    connectionFactory =
        (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
    connection = connectionFactory.createConnection();
    connection.start();
    session = connection.createSession(useTransaction,
        Session.AUTO_ACKNOWLEDGE);
    destination = session.createQueue("TEST.QUEUE");
    producer = session.createProducer(destination);
    message = session.createTextMessage("this is a test");
    producer.send(message);
  } catch (JMSException jmsEx) {
...
  } finally {
    producer.close();
    session.close();
    connection.close();
  }
}

In listing 2.8, first an initial context is created. Most typically, the context is fetched from a provider using a JNDI path. This one is for demonstration purposes only. Using the initial context, a JMS connection factory is acquired using the unique name to identify it. Using the connection factory, a JMS connection is created and started. This is a requirement so that the JMS client begins to communicate with the broker. Using the JMS connection, a JMS session is created and the example uses auto-acknowledgement of messages. A JMS queue is then created via the JMS session object. Next, a JMS message producer is created using the session and the destination. Then a simple text message is created via the session and sent via the message producer. The last action taken in this example is to close all the objects that were being used.

The example in listing 2.8 demonstrates the simplest steps to create a JMS producer and send a message to a destination. Note that there’s no concern whether a JMS consumer is on the other end waiting for the message. This mediation of messages between producers and consumers is what MOMs provide and is a big benefit when creating JMS applications. There was no special consideration to achieve this result either. The JMS APIs make this task simple. Now that the message has been sent to the destination, a consumer can receive the message. The following listing demonstrates the steps for creating a JMS consumer and receiving the message.

Listing 2.9. Receiving a JMS message synchronously
public class MySyncMessageConsumer {
...
  ConnectionFactory connectionFactory;
  Connection connection;
  Session session;
  Destination destination;
  MessageConsumer consumer;
  Message message;
  boolean useTransaction = false;
  try {
    Context ctx = new InitialContext();
    connectionFactory =
        (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
    connection = connectionFactory.createConnection();
    connection.start();
    session = connection.createSession(useTransaction,
        Session.AUTO_ACKNOWLEDGE);
    destination = session.createQueue("TEST.QUEUE");
    consumer = session.createConsumer(destination);
    message = (TextMessage) consumer.receive(1000);
    System.out.println("Received message: " + message);
  } catch (JMSException jmsEx) {
...
  } finally {
    producer.close();
    session.close();
    connection.close();
  }
}

The example in listing 2.9 is similar to listing 2.8 because both need the same setup up until the creation of the JMS message consumer. After that step, the consumer is used to receive the message that was sent to the destination in the previous example and the message is printed out. The last action is to close all the objects that were being used. Again, note that no timing consideration was needed to make sure that the producer is there sending a message. All mediation and temporary storage of the message is the job of the JMS provider implementation. Listing 2.9 demonstrates the synchronous consumption of messages. This means that the JMS consumer sends a request to the JMS provider to receive a message and waits for a response for the given amount of time. The consumer must poll for messages over and over again in a loop. Consuming messages using synchronous polling of a destination isn’t the only flavor of message consumption in JMS.

The JMS API also provides the ability to asynchronously receive messages. The JMS provider will push messages to the consumer. A simple example of asynchronous message consumption follows.

Listing 2.10. Receiving a JMS message asynchronously
public class MyAsyncMessageConsumer implements MessageListener {
...
  ConnectionFactory connectionFactory;
  Connection connection;
  Session session;
  Destination destination;
  MessageProducer producer;
  Message message;
  boolean useTransaction = false;
  try {
    Context ctx = new InitialContext();
    connectionFactory =
        (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
    connection = connectionFactory.createConnection();
    connection.start();
    session = connection.createSession(useTransaction,
        Session.AUTO_ACKNOWLEDGE);
    destination = session.createQueue("TEST.QUEUE");
    consumer = session.createConsumer(destination);
    consumer.setMessageListener(this);
  } catch (JMSException jmsEx) {
...
  } finally {
    producer.close();
    session.close();
    connection.close();
  }

  public void onMessage(Message message) {
    if (message instanceof TextMessage) {
      System.out.println("Received message: " + message);
    }
  }
}

The difference between listings 2.9 and 2.10 is the implementation of the onMessage method from the MessageListener interface and the registration of the implementation with the JMS provider. Asynchronously receiving messages as shown in listing 2.10 is extremely powerful. It means that the consumer no longer needs to manually poll for messages repeatedly. Instead, the MessageListener implementation is registered with the JMS provider to act as a sort of callback where the message will be delivered automatically to the onMessage method in an asynchronous manner.

 

A note on multithreading in JMS applications

The JMS spec specifically defines concurrency for various objects in the JMS API and requires that only a few objects support concurrent access. The ConnectionFactory, Connection, and Destination objects are required to support concurrent access, whereas the Session, MessageProducer, and MessageConsumer objects don’t support concurrent access. The point is that the Session, MessageProducer, and MessageConsumer objects shouldn’t be shared across threads in a Java application.

 

There’s one additional aspect to the JMS APIs for consuming messages. It involves asynchronous message consumption but concerns the EJB API known as message-driven beans.

2.5.2. Message-driven beans

Message-driven beans (MDBs) were born out of the EJB 2.0 spec. The motivation was to allow simple JMS integration into EJBs, making asynchronous message consumption by EJBs almost as easy as using the standard JMS APIs. Through the use of a JMS MessageListener interface, the EJB automatically receives messages from the JMS provider in a push style. An example of a simple MDB is shown here.

Listing 2.11. A simple message-driven bean example
import javax.ejb.EJBException;
import javax.ejb.MessageDrivenBean;
import javax.ejb.MessageDrivenContext;
import javax.jms.Message;
import javax.jms.MessageListener;

public class MyMessageProcessor
    implements MessageDrivenBean, MessageListener {
    public void onMessage(Message message) {
        TextMessage textMessage = null;

        try {
            if (message instanceof TextMessage) {
                textMessage = (TextMessage) message;
                System.out.println("Received message: " + msg.getText());
                processMessage(textMessage);
            } else {
                System.out.println("Incorrect message type: " +
                    message.getClass().getName());
            }
        } catch (JMSException jmsEx) {
            jmsEx.printStackTrace();
        }
    }

    public void ejbRemove() throws EJBException {
        // This method is called by the EJB container
    }

    public void setMessageDrivenContext(MessageDrivenContext ctx)
        throws EJBException {
        // This method is called by the EJB container
    }

    private void processMessage(TextMessage textMessage) {
        // Do some important processing of the message here
    }
}

Note that the MyMessageProcessor class in listing 2.11 implements both the Message-DrivenBean interface and the MessageListener interface. The MessageDrivenBean interface requires an implementation of the setMessageDrivenContext() method and the ejbRemove() method. Each of these methods is invoked by the EJB container for the purposes of creation and destruction of the MDB. The MessageListener interface contains only a single method named onMessage(). The onMessage() method is invoked automatically by the JMS provider when a message arrives in a destination on which the MDB is registered.

In addition to allowing the EJB container to manage all necessary resources including Java EE resources (such as JDBC, JMS, and JCA connections), security, transactions, and even JMS message acknowledgement, one of the biggest advantages of MDBs is that they can process messages concurrently. Not only do typical JMS clients need to manually manage their own resources and environment, but they’re usually built for processing messages serially—one at a time (unless they’re specifically built with concurrency in mind). Instead of processing messages one at a time, MDBs can process multiple messages at the same time because the EJB container can create as many instances of the MDBs as are allowed by the EJB’s deployment descriptor. Such configuration is typically specific to the Java EE container. If you’re using a Java EE container for this, consult the documentation for the container on how this is configured in the EJB deployment descriptor.

A disadvantage of MDBs is their requirement of a full Java EE container. Just about every EJB container available today can support MDBs only if the entire Java EE container is used. MDBs are extremely useful when using a full Java EE container, but there’s an alternative that doesn’t require the full Java EE container. Using the Spring Framework’s JMS APIs makes developing message-driven POJOs (MDPs) easy. These are Plain Old Java Objects (POJOs) that act as if they’re message driven. This style of development has become popular in the Java development community because it avoids the overhead of using a full Java EE container. Such development with the Spring Framework will be discussed in further detail in chapter 7.

 

Not every EJB container requires a full Java EE container—try OpenEJB

At the time of this writing, nearly all EJB containers on the market require a full Java EE container to support MDBs. The exception to this rule is Apache OpenEJB (http://openejb.apache.org/). OpenEJB supports MDBs from the EJB 1.1 spec, the EJB 2 spec, and the EJB 3 spec in OpenEJB’s embedded mode as well as in its standalone mode. OpenEJB can be embedded inside of Apache Geronimo (http://geronimo.apache.org/), Apache Tomcat (http://tomcat.apache.org/), or your own Java application and will still provide support for MDBs.

 

2.6. Summary

The impact of enterprise messaging on the business world has been significant. Enterprise messaging and the concepts surrounding it have influenced the development of many additional technologies and concepts. Without enterprise messaging, developers wouldn’t have an option beyond synchronous calls for application development, and the concept of decoupling an application design wouldn’t exist in nearly the same form. SOA, CEP, and many other higher-level concepts built on top of enterprise messaging wouldn’t have come about. Furthermore, the JMS spec wouldn’t exist today without enterprise messaging.

The JMS spec has had a tremendous effect on the Java world, making messaging a first-class citizen and making it available to all Java developers. This was an important step in allowing Java to join the business of mission-critical applications, because it provides a standardized manner to utilize messaging. The examples provided in this chapter are admittedly short and simple in order to get your feet wet with JMS. As you move though the rest of the book, richer examples will be discussed and made available for download.

Now that you have a basic understanding of JMS and what it provides, the next step is to review the samples for the book. Chapter 3 provides an introduction to the sample applications that will be used throughout the rest of the book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.243.184