Chapter 9 Overview of JMS
Message-oriented middleware (MOM) is software for building distributed applications that communicate by sending messages to each other. JMS is a standardised Java API for MOM products. This chapter provides an overview of JMS. Then, the next chapter explains how Config4JMS provides a simplified “wrapper” for JMS.
9.2 Terminology and Concepts
In JMS terminology, an application that sends a message is called a producer, and an application that receives a message is called a consumer (or sometimes a subscriber). It is common for a JMS-based application to act as both a producer and a consumer at the same time. JMS offers two different approaches for message-based communication: queues and topics.
A queue provides one-to-one communication. A producer sends a message to a queue, and the message is stored on the queue until it can be delivered to one consumer.
A topic provides one-to-many communication. A producer application sends a message to a topic. The message will be delivered to all the consumer applications that are currently registered as subscribers of that topic. By default, a subscriber registration lasts only while a consumer application is running—when a consumer application terminates, any subscriber registrations it had will lapse automatically. However, if a consumer application registers itself as a durable subscriber to a topic, then the topic will store messages for a durable subscriber that is currently not running, and will deliver those messages later when the durable subscriber is restarted.
It is possible to imagine many different qualities of service that might be provided by a MOM product. For example, is there a maximum size of message that can be sent to a queue or topic? Is there a maximum number of messages that can be stored in a queue? Should a not-yet-delivered message be discarded if it has not been delivered within a specific period of time? Should messages transmitted across a network be sent in an encrypted format? Or perhaps in a compressed format?
JMS takes a two-pronged approach to standardising such qualities of service.
- Most of the interfaces in the JMS specification contain set<Name>() operations that can be invoked to set a desired quality of service.
The JMS standardisation committee realised there was too much
variation across the proprietary features in existing MOM
products to be able to standardise everything with
set<Name>() operations. However, they felt most of the
proprietary features that could not be standardised in this
manner were confined to the concepts of a destination
(the genetic term for a queue or topic) and a connection
(so called because it is often implemented as a socket
connection from an application to a MOM product).
The committee decided that portability for JMS applications could be achieved by specifying the proprietary qualities of service for connections and destinations outside application code. It was envisaged that an administrator would pre-create destination objects with proprietary qualities of service, and advertise these destination objects in a naming service. A JMS application would then retrieve pre-created destination objects from the naming service. Likewise, an administrator would pre-create a connection factory with proprietary qualities of service, and advertise it in a naming service. A JMS application would retrieve this factory object from the naming service and use it to create connections. A connection object created in this manner would have whatever qualities of service its factory had.
The JMS specification uses the term administered objects to refer to connection factories and destinations because of the intention that such objects would be created by an administrator rather than by application code.
Another portability issue that the JMS standardisation committee had to consider was thread safety. The entire API of one MOM product might be thread-safe, but another MOM product might provide a mixture or thread-safe and thread-unsafe operations. The approach taken to deal with this was to introduce the concept of a session object that application developers should assume is not thread-safe. The developer of a multi-threaded application is required to create multiple session objects: one for each thread.
9.4 Problems with JMS
JMS is good but, like any technology, it is not perfect. In this section I discuss some irritations and imperfections in JMS that I have discovered. I do this to motivate the development and benefits of Config4JMS.
9.4.1 Books and Manuals Advocate the Legacy API
The JMS 1.0 specification defined one set of interfaces for communication via queues, and a separate set of interfaces for communication via topics.
The JMS 1.1 specification provides a unified set of interfaces that can be used for communication with both queues or topics. For backwards compatibility, the JMS 1.1 specification continues to support the original (queue-specific and topic-specific) interfaces.
There are several reasons why developers should use the JMS 1.1 API in new applications. First, the JMS 1.1 specification warns that the original API might be deprecated in a future version of the specification. Second, the unified API provides more opportunities for optimisation in JMS products. Third, the unified API in JMS 1.1 is more concise and easier to use than the original API.
Despite the significant benefits of the unified API, books and product manuals continue to use the original API in worked examples. This can result in developers who are new to JMS needlessly learning an outdated, verbose API instead of the newer, more concise API.
9.4.2 Confusingly Many Initialisation Steps
A portable JMS application must complete many initialisation steps before it can do “real” work. For example, an application acting as both a producer and a consumer typically carries out nine initialisation steps:
- Connect to a naming service.
- Retrieve one or more Destination objects from the naming service.
- Retrieve a ConnectionFactory object from the naming service.
- Invoke createConnection() on the ConnectionFactory object to create a Connection object.
- Create two Session objects by invoking createSession() twice on the Connection object.
- Invoke createProducer() on one of the Session objects (passing a Destination object as a parameter) to create a Producer object.
- Invoke createConsumer() on the other Session object (passing a Destination object as a parameter) to create a Consumer object.
- Create and register a MessageListener object on the Consumer object. (A MessageListener is a callback interface whose onMessage() operation is invoked whenever a Consumer receives a message.)
- Invoke start() on the Connection object.
Psychological research indicates that most people can remember only about seven new pieces of information at a time . Because of this limit, the nine-step initialisation sequence provides a hurdle for new developers to master. This is a shame, because once initialisation is complete, using JMS is straightforward.
9.4.3 Requiring Programmers to Learn Administration Skills
The first initialisation step discussed in Section 9.4.2 is to connect to a naming service that has been populated with ConnectionFactory and Destination objects. As I explained in Section 9.3, the intention is that those objects will be created with proprietary qualities of services (via proprietary administration tools) by an administrator; in this way, application code is not polluted with the setting of proprietary qualities of service.
When an application is being deployed in a production environment, there may well be an administrator available to create the Destination and ConnectionFactory objects, and advertise them in a naming service. However, such an administrator is unlikely to be available during initial application development. This means that JMS developers have to learn how to carry out such administration tasks themselves. In fact, a developer who is starting to learn JMS will have to learn those administration skills before being able to write a portable, “Hello, World”-type JMS application. This is yet another hurdle for new developers to master.
9.4.4 Only Partial Portability in JMS
In Section 9.3, I outlined the approach used in the JMS specification to provide portability of JMS-based applications. Unfortunately, the approach used is only partially successful. It is very easy for developers to feel tempted—or sometimes be required—to use vendor-proprietary functionality in a JMS-based application.
In the relatively short period of time I have spent using JMS, I noticed four reasons why a developer might resort to using a proprietary API.
First, obtaining Destination and ConnectionFactory objects from a naming service is inconvenient—especially for a developer new to JMS who does not want to have to spend time learning JMS administration commands before being able to write an application. It is usually more convenient to use vendor-proprietary functions to create Destination and ConnectionFactory objects directly.
Second, it is common for JMS products to offer qualities of service above and beyond those defined in the JMS specification. If these qualities of service are related to, say, Session, Producer or Consumer, then it is natural for a vendor to provide proprietary set<Name>() operations on those types. Put simply, not all proprietary qualities of service can be encapsulated in administered objects.
Third, when a JMS-related operation fails, it throws a (subtype of) JMSException. The developer of an application might wish to process a caught exception in one of several ways, depending on the nature of the exception. However, the JMS specification states that two out of the three pieces of information provided by JMSException are proprietary to a JMS vendor. Because of this, a developer may need to rely on vendor-proprietary information contained in an exception when deciding how to process it.
Finally, JMS specifies that a message is composed of three parts: (1) header fields, (2) arbitrary properties (that is, name=value pairs), and (3) a body. The intended use of (2) is to support flexible message selection in consumer applications. For example, a producer application running in, say, London, might add location=London to a message’s properties before sending the message. A Consumer application could then use the message selector "(location = ’London’)" to ensure it receives only messages with that property value. The JMS specification reserves property names starting with "JMS_<vendor>" for use by JMS vendors. Some vendors use such properties to specify a proprietary quality of service on a per message basis. A developer who wishes to make use of a proprietary, per-message quality of service will have to modify the code of a producer application so it sets the proprietary property prior to sending a message.
9.5 Critique: The 80/20 Principle
You may be familiar with some variations of the Pareto Principle, also known as the 80/20 Principle:
- 80% of the wealth in a country is owned by 20% of the population.
- 80% of CPU time is spent in 20% of the code.
- A business gets 80% of its income from 20% of its customers.
- 80% of the work in a company is done by 20% of the employees.
Sometimes the 80/20 Principle can suggest how to make large improvements for a relatively small investment of effort. For example, if you want to optimise an application for speed, then item 2 in the above list suggests you should use a profiling tool to identify the most CPU-intensive parts of the application, so you can focus your optimisation efforts on them.
There is a little-known variant of the 80/20 Principle that I often find useful:
80% of a product’s complexity is in 20% of its functionality.
If you want to increase a product’s ease of use, then you should identify its few areas of disproportionate complexity, so you can focus your ease-of-use efforts on them.
The problems discussed in Section 9.4 account for most of the complexity in JMS, but only a minority of its functionality. The goal of Config4JMS, which is discussed in the next chapter, is to put a “simplification wrapper” around those disproportionately complex parts of JMS.