Level: Intermediate Nicholas Whitehead (nwhitehe@yahoo.com), Java architect, finetix LLC
01 Feb 2002 The popularity of the Java Message Service has been on an upswing lately, perhaps bolstered by the support of several major players in the world of real-time messaging. As an increasing number of vendors jump on the JMS bandwagon, it makes sense to ensure that your JMS code will work unmodified across multiple proprietary implementations. With a few simple steps, Java architect Nicholas Whitehead shows you how to combine JMS, the Java Naming and Directory Interface, and a well-made properties file to build vendor-neutral JMS solutions.
The Java Message Service (JMS) specification lays out a standard for Java-based point-to-point (P2P) and publish/subscribe (P/S) messaging. Sun currently lists 12 licensed implementors of JMS and 16 non-licensed
implementors. Architecturally, JMS is similar to the Java Database Connectivity (JDBC) API, because they both define a small number of classes but a large collection of interfaces. These interfaces exist to be implemented, and compliant implementations will behave the same.
In the case of most databases, the similarity in behavior ends with the JDBC interface implementation. Differences in SQL compliance levels and the use of proprietary procedural SQL extensions (such as Oracle's PL/SQL and Sybase's Transact-SQL) can require substantial differences in the code written to access and use database services.
Not so with JMS. With minimal effort and by following the procedures I recommend in this article, you can make your JMS client code blissfully unaware of the vendor implementation you're using. While I assume you have a basic understanding of JMS message processing, we'll start the discussion with a brief review of the basic concepts and terminology.
The JMS architecture
The basis for sending or receiving a message is a connection, which is responsible for allocating resources outside the JVM. A JMS vendor will typically implement at least a QueueConnection for P2P transactions and a TopicConnection for P/S transactions. These connections supply a Session, which is a construct for managing the sending and receiving of messages.
The basic constructs for P2P transaction management are QueueSender and QueueReceiver. The basic constructs for P/S transaction management are TopicSubscriber and TopicPublisher. Topic and queue objects
encapsulate specific information that directs the target and source of each message. This hierarchy is
shown in Figure 1:
Figure 1. JMS class hierarchy
Other constructs such as request/response support classes and features specific to application servers can be found in the JMS standard (see Resources).
Different types of connection setup
Because connection is the entry point to interaction with a JMS server, each implementation of a connection interface must know how to connect to an instance of its own JMS server. Because the details of the underlying connection protocol tend to be different for each vendor, the information required to set up an active connection is different for each vendor.
Most vendors allow a connection to be set up dynamically. That is to say, they define the constructors to the connection classes as public, allowing the programmer to define the required connection information. Most vendors supply factory
classes that will return a connection upon being invoked.
In the case of connection factories, the factory class can return a connection that has been pre-loaded with the proprietary connection information. A vendor-defined factory class will expose methods that allow the programmer to set the connection parameters. These connection parameters dictate the nature of the connections returned from the factory.
Where vendor APIs differ
To make all of this more concrete, let's look at the constructors, connection factories, and setup methods for several implementations of QueueConnection and QueueConnectionFactory. (Note that there are many overloaded constructors in some cases; I have illustrated just one for each case.)
IIT SwiftMQ 2.1.3 QueueConnectionFactory constructor parameters
-
java.lang.String socketFactory: The class name of the socket factory
-
java.lang.String hostname: The JMS server's host name
-
int port: The JMS server's port
-
long keepalive: The keep-alive interval
The following code shows how you create a SwiftMQ QueueConnectionFactory object:
QueueConnectionFactory qcf = (QueueConnectionFactory) new
com.swiftmq.jms.ConnectionFactoryImpl
("com.swiftmq.net.PlainSocketFactory", "myhost",4001,60000);
|
Progress SonicMQ 3.5 QueueConnection constructor parameters
-
java.lang.String brokerURL: The URL (in the form [protocol://]hostname[:port])
-
java.lang.String connectID: The ID string to identify the connection
-
java.lang.String username: The default username
-
java.lang.String password: The default password
Here is the sample code to create a Progress SonicMQ QueueConnectionFactory object:
progress.message.jclient.QueueConnection queueConnection = new
progress.message.jclient.QueueConnection("tcp://myhost:2506",
"ServiceRequest", "username", "password");
|
MQSeries (MA88)
The final example we'll look at is IBM MQSeries implementation. MQSeries does not make use of connection constructors. Instead, to dynamically create a connection, you must construct a connection factory, which in turn provides methods to supply a connection. The code to create the parameterless constructor is as follows:
MQQueueConnectionFactory = new
MQQueueConnectionFactory();
|
The connection factory's constructor is parameterless, so the factory has mutator methods that can be invoked to control the properties of the connection the factory will supply:
-
setTransportType(int x): Set the transport type to one of the following options:
-
JMSC.MQJMS_TP_BINDINGS_MQ: Used when the MQSeries server is on the same host as the client
-
JMSC.MQJMS_TP_CLIENT_MQ_TCPIP: Used when the MQSeries server is on a different host from the client
-
setQueueManager(String x): Set the name of the queue manager
-
setHostName(String hostname): For client only, set the name of the host
-
setPort(int port): Set the port for a client connection
-
setChannel(String x): For client only, set the channel to use
Here's the sample code to create an MQSeries QueueConnectionFactory and acquire a connection to a specific queue manager:
com.ibm.mq.jms.MQQueueConnectionFactory factory = new
com.ibm.mq.jms.MQQueueConnectionFactory();
factory.setQueueManager("QMGR");
com.ibm.mq.jms.MQQueueConnection connection =
factory.createQueueConnection();
|
JNDI's role in creating vendor-neutral code
As our brief review shows, each vendor employs its own distinct set of connection parameters. So, how do you transparently support all of them in your code? The standard solution is to use a naming service to persist a pre-configured ConnectionFactory. At run time, your code can retrieve the ConnectionFactory and the connections returned from it will be able to transparently connect to your JMS server. The maintenance and rebuild of your code is eliminated in favor of simply maintaining correctly configured connection factories in your naming service.
The Java Naming and Directory Interface (JNDI) is the most common way to interface with naming services. JNDI is similar to JMS in that it simply defines a set of interfaces to be implemented. All naming services that implement the JNDI can be accessed under a single, standard API.
JNDI is central to our efforts to write vendor-independent code, because it supplies a vendor-neutral way of accessing naming services. This lets us concern ourselves only with writing code to retrieve the correct objects from the naming service, without worrying about any of the proprietary implementations outlined in the previous section.
Connecting to a JMS server
By creating a connection factory, pre-configuring it, and binding it to your naming service you can conceal vendor-specific connection parameters in your messaging service. As far as your code is concerned, you are using generic
javax.jms.Connection objects. The vendor implementation is hidden behind the interface.
The JMS specification refers to objects that are created by an administrator and contain configuration information for use by JMS clients as JMS administered objects. Administered objects are not dependent on JNDI, but it is implicit that they can be bound to and looked up in a JNDI namespace.
Listings 1 and 2 show two different methods to connect to a JMS server (in this case SwiftMQ): one using vendor-dependent code and the other using vendor-neutral code.
Listing 1. A vendor-dependent connection method
1.QueueConnectionFactory queueConnectionFactory =
(QueueConnectionFactory) new
com.swiftmq.jms.ConnectionFactoryImpl
("com.swiftmq.net.PlainSocketFactory", "localhost",4001,60000);
2.QueueConnection queueConnection =
queueConnectionFactory.createQueueConnection();
|
Listing 2. A vendor-neutral connection method
1.Properties p = new Properties();
2.p.put(Context.INITIAL_CONTEXT_FACTORY,
"com.swiftmq.jndi.InitialContextFactoryImpl");
3.p.put(Context.PROVIDER_URL,"smqp://localhost:4001");
4.ctx = new InitialContext(p);
5.qcf = (QueueConnectionFactory)ctx.lookup("MyQCF");
6.oQueueConnection queueConnection =
queueConnectionFactory.createQueueConnection();
|
How the code breaks down
The first thing you'll notice is that the vendor-neutral code has a few more lines. This is because we have to connect to the naming service. Keep in mind, however, that you might only need to connect to the naming service once in an
entire program, so the extra few lines are worth it. (Just be sure to reuse the naming service rather than instantiating a remote context every time you need to employ one.)
The interaction with and preparation of the naming service is critical to writing vendor-neutral messaging code. In the vendor-dependent code example, we've used the QueueConnectionFactory constructor of the SwiftMQ implementation to create a factory that will give us a connection. For this implementation, we not only have to include vendor-proprietary classes, but we also have to pass the QueueConnectionFactory constructor
vendor-specific parameters, as shown in Line 1 of Listing 1.
In the vendor-neutral example, we have no vendor-specific code, but we do have to know the initial context factory and the provider URL of the naming service, as well as the binding name of the QueueConnectionFactory. In the case of the binding name, proper naming service maintenance will allow you to bind objects into your JNDI tree from any vendor, so while the vendor may change, the binding name doesn't have to. As for the JNDI context, it is common practice to store the parameter strings (Lines 2 and 3 of Listing 2) in a properties file and read them in as needed. This way, changing JMS vendors is a simple matter of changing the properties file.
It is also interesting to note that this technique gives you flexibility and portability with regard to naming services. Many JMS vendors (such as Fiorano and SwiftMQ) provide their own JNDI service, but you may want to separate the naming service from the JMS service. (For example, you may want to store your connection factories in a centralized LDAP server.)
The following are examples of property file entries that will result in different JNDI connections.
SwiftMQ JNDI service
-
java.naming.provider.url=smqp://myhost:4001
-
java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl
IBM WebSphere JNDI service
-
java.naming.provider.url=iiop://myhost:9001
-
java.naming.factory.initial=
com.ibm.websphere.naming.WsnInitialContextFactory
iPlanet directory server (LDAP)
-
java.naming.provider.url=ldap://myhost:389
-
java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
BEA WebLogic JNDI service
-
java.naming.provider.url=t3://myhost:7001
-
java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
File System JNDI service
-
java.naming.provider.url=file:/tmp/stuff
-
java.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory
Note that while your source code may not reference vendor classes directly, the vendor classes are loaded into the JVM dynamically by name, so they must be in your program's classpath at run time. This applies to both JNDI and JMS classes.
Setting up the properties file
So, at this point we know how to connect to a JNDI service and acquire a connection from different JNDI and JMS implementations without having to recompile our code. Let's pull together the pieces we have so far, with a look at how the properties file is set up and the role it plays in enabling a JNDI connection.
The base class for making a JNDI connection is javax.naming.InitialContext. While there
are subclasses of InitialContext that are specific to directory operations, such as InitialDirContext, the generic class will do the job. When the InitialContext is constructed, it may derive the JNDI parameter values from the environment (system properties or applet parameters) or it may look for a specific jndi.properties file.
Here's how this operation is explained in the J2SE 1.3.1 javadoc:
JNDI determines each property's value by merging the values from the following two sources, in order:
- The first occurrence of the property from the constructor's environment parameter and (for appropriate properties) the applet parameters and system properties
- The application resource files (jndi.properties)
Additional properties
So far we've only looked at two parameters: the URL of the provider and the InitialContext factory name. There are actually quite a few more properties that may be supplied. The most common ones, aside from the two we've looked at, are the user name and password that authenticate you for access to JNDI stores that may be secure. These parameters are:
-
java.naming.security.principal (username)
-
java.naming.security.credentials (password)
Loading the file
I recommend that you place all your application's run-time configuration parameters in one application
properties file and include the JNDI parameters there. Having all your parameters in one place
eliminates uncertainty. Then you have several options to load the application properties file. I
have outlined two for examples. The file can be loaded in as a resource bundle,
or you can pass the name and location of the properties file in as a command-line parameter. Each
of these approaches has different benefits.
Passing the file location in as a command-line parameter is the easiest way to configure your code. The parameters can be changed by simply modifying the startup of the application.
Loading the file in as a resource bundle has two advantages:
- Different resource bundles can be loaded according to the locale of the JVM. For example, the application_en_US.properties file may point to a JNDI service in New York, while the application_fr.properties file points to a JNDI service in Paris.
- Loading properties from a resource bundle is an architecture- and platform-independent way of loading a properties file. Because resource bundles are loaded from the classpath, the code isn't dependent on being able to read the JVM's
command-line parameters. Also, some components such as EJB components should not make direct use of file I/O, so the resource bundle may provide a more convenient way to load the contents of a properties file.
To avoid confusion with conflicting environment settings, I always set a properties instance with the JNDI values I've read out of my properties file.
Properties file initialization and JNDI lookup
The code listings in this section demonstrate both types of properties initialization (command-line
parameter and resource bundle) as well as a generic JNDI lookup. To start, we'll look at a sample configuration file called PropertiesManagement.properties, shown in Listing 3:
Listing 3. PropertiesManagement.properties
java.naming.provider.url=smqp://localhost:4001
java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl
java.naming.security.principal=admin
java.naming.security.credentials=secret
com.nickman.neutraljms.QueueConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.TopicConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.Queue=testqueue@router1
com.nickman.neutraljms.Topic=testtopic
|
The first four items in the file are the JNDI environment properties. I've added the authentication properties for clarity. The last four items are the naming service names, where the JMS objects are bound. We will use these names to retrieve connection factories, queues, and topics. If you were using LDAP, the names would probably not be as simple. You might see something like this:
-
java.naming.provider.url = ldap://myhost:389/o=nickman.com
-
com.nickman.neutraljms.QueueConnectionFactory =
cn=myQueueConnectionFactory,ou=jmsTree
Properties file definition
Now we'll look at the code to read the properties file. As previously discussed, you have two options for how to determine the JNDI connection parameters. Listing 4 is sample code for retrieving JNDI properties:
Listing 4. Retrieving JNDI properties
package com.nickman.jndi;
import javax.naming.*; // For JNDI Interfaces
import java.util.*;
import java.io.*;
import javax.jms.*;
public class PropertiesManagement {
Properties jndiProperties = null;
Context ctx = null;
public static void main(String[] args) {
PropertiesManagement pm = new PropertiesManagement(args);
.
.
public PropertiesManagement(String[] args) {
jndiProperties = new Properties();
if(args.length>0) {
try {
loadFromFile(args[0]);
.
.
} else {
try {
loadFromResourceBundle();
.
.
private void loadFromFile(String fileName) throws Exception {
FileInputStream fis = null;
try {
fis = new FileInputStream(fileName);
jndiProperties.load(fis);
} finally {
try { fis.close(); } catch (Exception erx){}
}
}
private void loadFromResourceBundle() throws Exception {
String key = null;
String value = null;
ResourceBundle rb =
ResourceBundle.getBundle("PropertiesManagement");
Enumeration enum = rb.getKeys();
while(enum.hasMoreElements()) {
key = enum.nextElement().toString();
value = rb.getString(key);
jndiProperties.put(key, value);
}
}
|
You can download the entire source code file for reference as we work through it here.
How the code breaks down
Listing 4 shows the code for loading the properties file two different ways. If a command-line parameter is passed in, the code assumes it is the fully qualified properties file name, and the properties are loaded using the loadFromFile(String fileName) method.
The class may have been invoked as follows:
java com.nickman.jndi.PropertiesManagement
c:\config\PropertiesManagement.properties
|
If no command-line parameter is passed, the code will invoke the loadFromResourceBundle() method. This method will find the properties file on the CLASSPATH, so it is necessary to put the directory with
this file in the classpath. Either way, the properties are loaded into the properties variable jndiProperties.
Connecting to the JNDI service
Listing 5 demonstrates the connection to a JNDI service:
Listing 5. JNDI connection
public void connectToJNDI() throws javax.naming.NamingException {
// jndiProperties was loaded from PropertiesManagement.properties
ctx = new InitialContext(jndiProperties);
System.out.println("Connected to " +
ctx.getEnvironment().get(Context.PROVIDER_URL));
}
|
The above connection code is fairly straightforward. The jndiProperties variable is passed into the InitialContext constructor and the resulting Context
is a "handle" to the JNDI service. It is useful to note that the interface javax.naming.Context
contains a set of constants to represent all the available environment properties.
With the Context established, we can get on with looking up the JMS
objects, as shown in Listing 6:
Listing 6. The JNDI lookups
public QueueConnectionFactory lookupQueueConnectionFactory()
throws javax.naming.NamingException {
return
(QueueConnectionFactory)ctx.lookup(jndiProperties.get
("com.nickman.neutraljms.QueueConnectionFactory").toString());
}
public Queue lookupQueue() throws javax.naming.NamingException {
return
(Queue)ctx.lookup(jndiProperties.get
("com.nickman.neutraljms.Queue").toString());
}
|
The lookups are simply a matter of invoking the lookup(String name) method of the Context and passing in the name where the object we want is bound. The returned object must be cast to the correct class, which in this case will be one of the standard javax.jms
interfaces.
The Destination interface
javax.jms.Destination is an interface that encapsulates a specific target to which a message will be sent. Both the Queue and Topic interfaces extend the Destination interface. Because Destinations are JMS-administered objects, Queues and Topics are too.
You will notice that the JMS API contains two methods in the Topic and Queue session classes:
-
Topic TopicSession.createTopic(java.lang.String topicName)
-
Queue QueueSession.createQueue(java.lang.String topicName)
So, the question is this: why should you bother to save a queue or a topic in the JNDI when you could simply refer to it through a single string? The reasons are subtle; to get at them, I'll quote directly from the javadoc:
A Destination object encapsulates a provider-specific address. The JMS
API does not define a standard address syntax. Although a standard address syntax was considered, it was decided that the differences in address semantics between existing message-oriented middleware (MOM) products were too wide to
bridge with a single syntax.
Since Destination is an administered object, it may contain provider-specific configuration information in addition to its address.
Briefly, this means that we can hide specific JMS provider details behind a simple name that we use to look up a namespace in JNDI. I've also found that having a layer of indirection between the JMS client and the actual JMS destination gives the architecture additional flexibility. The client code can refer to a namespace in JNDI called myQueue but the administrator can set the object at that namespace to be any queue destination from any vendor. This idea is illustrated in Figure 2:
Figure 2. Destination flexibility
The trouble with topics
The publish-and-subscribe framework in JMS defines some functionality that could throw a wrench into the works of our effort to remain vendor neutral. Many JMS servers support the idea of a hierarchical namespace. This allows P/S
messages to be categorized into a hierarchy. When a topic-subscribing client connects to the JMS server, it can request messages that fit into a specific section of the hierarchy. As an example, consider the hierarchy outlined in
Figure 3:
Figure 3. A sample hierarchy
To understand this a little better, we'll use an example scenario. Say a subscriber client wants to subscribe to all US Equity Prices in a service. If this were a static subscription, the client might simply retrieve the JMS administrative object representing a topic that had been pre-configured to subscribe to US Equities. This would be ideal because different JMS vendors have different syntaxes for describing hierarchical subscriptions, and by hiding this
behind the JNDI-retrieved object, the client would remain ignorant of the underlying JMS implementation.
But consider a hierarchy that contains 50 different levels, offering hundreds of different "cells" to subscribe to. Also consider that the hierarchy might be dynamic, and that the administrators could be constantly adding to it. In such a case, it would be impractical for the JMS administrator to create all the JMS administrative objects necessary to represent all the possible subscriptions. Moreover, the hierarchy selection might need to be very flexible and dynamic to support the client application.
In this situation, it would make more sense for the topic names to be defined at run time. The trouble is, the syntax used to express the hierarchy could differ on a vendor-by-vendor basis. The following code fragments illustrate the subscription syntax used by three different vendors.
MQSeries JMS
Topic topicEqUs = topicSession.createTopic("topic://Prices/Equity/US");
Topic topicEqAll = topicSession.createTopic("topic://Prices/Equity/*");
|
SonicMQ 3.5
Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.*");
|
SwiftMQ 2.1.3
Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.%");
|
Note that the MQSeries JMS implementation employs a different topic delimiter than the other two implementations (it uses a forward slash while almost every other vendor uses a point).
Aside from the characters listed above, there are other vendor-specific strings that may appear in a topic name. For example, SonicMQ uses the pound sign to delimit a superior hierarchy and MQSeries employs a topic-name suffix to
express options normally reserved for the API. Almost all JMS providers employ different wild-card characters, making it tough to uniformly define the topic names at run time. Fortunately, there is a workaround to this problem: we store all the special characters in a reference source, load them from that source, and use them at run time.
The reference source could be your application properties file or it could be your JNDI service. To illustrate this workaround, we'll add the topic characters into our PropertiesManagement.properties file, as shown in Listing 7:
#Topic Delimiter For Sonic and Swift
com.nickman.neutraljms.TopicDelemiter=.
#Topic Delimiter for MQSeries
#com.nickman.neutraljms.TopicDelemiter=/
#Topic Wild Card For Sonic and MQSeries
com.nickman.neutraljms.TopicWildCard=*
#Topic Wild Card For Swift
#com.nickman.neutraljms.TopicWildCard=%
#Topic Prefix For MQSeries
#com.nickman.neutraljms.TopicPrefix=topic://
#Topic Prefix For All Others
com.nickman.neutraljms.TopicPrefix=
|
With these entries added, our topic subscription code would be vendor independent if it looked as follows:
String delim =
jndiProperties.get("com.nickman.neutraljms.TopicDelemiter").toString();
String wildcard =
jndiProperties.get("com.nickman.neutraljms.TopicWildCard").toString();
Strung prefix =
jndiProperties.get("com.nickman.neutraljms.TopicPrefix").toString();
Topic topicEqUs = topicSession.createTopic("Prices" + delim +
"Equity" + delim + "US");
Topic topicEqAll = topicSession.createTopic("Prices" + delim +
"Equity" + delim + wildcard);
|
Once you've mastered the external configuration shown in Listing 7 you can extend it to cover other vendor-specific options. For example, the JMS spec defines two delivery modes that a session can implement, but Sonic MQ supports
three additional delivery modes. By defining the delivery mode externally, you can maintain the vendor-neutrality of your code while also implementing Sonic MQ's proprietary extensions.
Conclusion
The methods outlined in this article are by no means comprehensive. My goal is to get you started on the path to implementing vendor-neutral JMS solutions. Furthermore, these techniques ought to improve your ability to integrate changes as you absorb new JMS implementations.
As you have probably noticed, all the techniques here rely strongly on the tactic of storing JMS-administered objects in JNDI. Further investigation of the different ways of working with JMS and JNDI together, as well as integrating a J2EE application server with a JNDI service, is left to a future article.
Download | Name | Size | Download method |
|---|
| j-jmsvendor.zip | | HTTP |
Resources
- Download the source file for this article.
- If you're just getting started with Java Message Service programming, check out Willy Farrell's popular tutorial "Introducing the Java Message Service" (developerWorks, August 2001).
- In "Should you use JMS in your next enterprise application?" (developerWorks, February 2002), columnist Brian Goetz outlines some of the benefits of using message queuing in Java applications, and explores the types of problems that can benefit the most from MQ technology.
- Daniel Drasin discusses the advantages, some potential pitfalls, and practical instructions for successfully setting up IBM MQSeries (now called WebSphere MQ) as a JMS server in his article "Get the message?" (developerWorks, February 2002).
- To learn more about the Java Message Service API, visit Sun Microsystems's JMS home page.
- Get under the hood with the JMS
javadoc.
- Also see the JNDI
javadoc.
- If you're looking for JMS programming tips, JGuru's JMS
FAQ has a lot to offer.
- The following JMS implementations were used in the writing of this article:
- You'll find hundreds of articles about every aspect of Java programming in the IBM
developerWorks
Java technology zone.
About the author  | |  |
Nicholas Whitehead is a Java architect at finetix LLC. He currently is working on a project for JP Morgan Chase in New York City. You can contact Nicholas at nwhitehe@yahoo.com.
|
Rate this page
|