Previous Up Next

Chapter 10  Config4JMS Functionality

10.1  Introduction

In this chapter, I discuss the functionality provided by Config4JMS. I start by explaining the structure of a Config4JMS configuration file. Then, I discuss the API of Config4JMS. Finally, I explain how use of Config4JMS can increase the portability of applications.

10.2  Syntax

Figure 10.1 shows an example of a configuration file used by Config4JMS.

Figure 10.1: Example Config4JMS configuration file
username        ?= ""; 
password        ?= ""; 
messageSelector ?= "";
chat {
    config4jmsClass = "org.config4jms.portable.Config4JMS";
    jndiEnvironment = [
        # name                               value
        #----------------------------------------------
        "java.naming.factory.initial",       "...",
        "java.naming.provider.url",          "...",
        "java.naming.security.principal",    .username,
        "java.naming.security.credentials",  .password,
    ];
    Topic.chatTopic {
        obtainMethod = "jndi#SampleTopic1";
    }
    ConnectionFactory.factory1 {
        obtainMethod = "jndi#SampleConnectionFactory1";
    }
    Connection.connection1 {
        createdBy = "factory1";
        create {
            userName = .username;
            password = .password;
        }
    }
    Session.prodSession {
        createdBy = "connection1";
        create {
            transacted = "false";
            acknowledgeMode = "AUTO_ACKNOWLEDGE";
        }
    }
    Session.consSession {
        createdBy = "connection1";
        create {
            transacted = "false";
            acknowledgeMode = "AUTO_ACKNOWLEDGE";
        }
    }

    TextMessage.chatMessage {
        createdBy = "prodSession";
        JMSExpiration = "10 hours";
        JMSPriority = "7";
        properties = [
            # name       type      value
            #------------------------------
             "location", "string", "London",
        ];
    }
    MessageProducer.chatProducer {
        createdBy = "prodSession";
        create.destination = "chatTopic";
        deliveryMode = "PERSISTENT";
        timeToLive = "2 minutes";
    }
    MessageConsumer.chatConsumer {
        createdBy = "consSession";
        create {
            destination = "chatTopic";
            messageSelector = .messageSelector;
            noLocal = "false";
        }
    }
}

The conditional assignment operator ("?=") is used to assign default values to the variables in the global scope. Those default values can be overridden by, for example, an application’s command-line options.

The chat scope contains Config4JMS-related configuration information for an application. Most of the entries within the chat scope are nested sub-scopes that specify details of how to create JMS-related objects. For example, the Topic.chatTopic scope specifies how to create a Topic object called chatTopic, and Session.prodSession specifies how to create a Session object called prodSession.

JNDI is an acronym for the Java Naming and Directory Interface. The jndiEnvironment variable specifies details of how to connect to a naming service.

The Topic.chatTopic scope contains the following variable:

obtainMethod = "jndi#SampleTopic1";

That variable specifies Config4JMS should obtain the chatTopic object by invoking lookup("SampleTopic1") on the naming service. An alternative setting for this variable might be:

obtainMethod = "file#/path/to/file/containing/a/serialised/java/object";

The createdBy variable in the Connection.connection1 scope specifies that the connection1 object is created by (invoking a factory method on) the object named factory1, which is of type ConnectionFactory. The create sub-scope specifies the parameter values to be used when invoking the factory operation.

In a similar way, the two Session objects are created by invoking a factory method on the connection1 object.

The MessageProducer.chatProducer scope specifies that the object called chatProducer is created by the prodSession object, and that the destination parameter passed to the factory operation is the chatTopic object. This scope also specifies values for two properties:

deliveryMode = "PERSISTENT";
timeToLive = "2 minutes";

Config4JMS invokes the setDeliveryMode() and setTimeToLive() operations on the object to set those properties. When doing do, Config4JMS converts "PERSISTENT" into the appropriate integer constant, and converts "2 minutes" into the appropriate number of milliseconds.

Although the example configuration file shows only two properties being set, Config4JMS can be used to set any of the properties defined in the JMS specification. As I will discuss in Section 10.4, it is possible for Config4JMS to also set properties that are proprietary to a JMS product.

10.3  API

The API of Config4JMS is defined in the org.config4jms.Config4JMS class, which is shown in Figure 10.2. For brevity, throws clauses are not shown in Figure 10.2.

Figure 10.2: Config4JMS API
package org.config4jms;
//--------
// Most operations can throw a Config4JMSException.
// However, the "throws" clause is omitted for brevity.
//--------
public abstract class Config4JMS {
    //--------
    // No public constructor. Use these create() operations instead.
    //--------
    public static Config4JMS create(String    cfgSource,
                                    String    scope,
                                    Map       cfgPresets);

    public static Config4JMS create(String    cfgSource,
                                    String    scope,
                                    Map       cfgPresets,
                                    String[]  typeAndNamePairs);
    //--------
    // Retrieve an object by name.
    //--------
    public Object            getObject(String type, String name);
    public ConnectionFactory getConnectionFactory(String name);
    public Connection        getConnection(String name);
    public Session           getSession(String name);
    public Destination       getDestination(String name);
    public Queue             getQueue(String name);
    public Queue             getTemporaryQueue(String name);
    public Topic             getTopic(String name);
    public Topic             getTemporaryTopic(String name);
    public MessageProducer   getMessageProducer(String name);
    public MessageConsumer   getMessageConsumer(String name);
    public QueueBrowser      getQueueBrowser(String name);

    //--------
    // Connection operations.
    //--------
    public void setExceptionListener(ExceptionListener listener);
    public void startConnections();
    public void stopConnections();
    public void closeConnections();


    //--------
    // Message operations
    //--------
    public Message createMessage(String name);
    public void applyMessageProperties(String name, Message msg);

    //--------
    // List the names of objects of different types.
    //--------
    public String[] listConnectionFactoryNames();
    public String[] listConnectionNames();
    public String[] listSessionNames();
    public String[] listDestinationNames();
    public String[] listQueueNames();
    public String[] listTemporaryQueueNames();
    public String[] listTopicNames();
    public String[] listTemporaryTopicNames();
    public String[] listMessageProducerNames();
    public String[] listMessageConsumerNames();
    public String[] listQueueBrowserNames();
    public String[] listMessageNames();
    public String[] listBytesMessageNames();
    public String[] listMapMessageNames();
    public String[] listObjectMessageNames();
    public String[] listStreamMessageNames();
    public String[] listTextMessageNames();

    //--------
    // Frequently used miscellaneous operations
    //--------
    public void          createJMSObjects();
    public Configuration getConfiguration();
    public boolean       isNoConnection(JMSException ex);

    //--------
    // Rarely used miscellaneous operations
    //--------
    public Object  importObjectFromJNDI(String path);
    public Object  importObjectFromFile(String fileName);
}

Rather than discuss every operation individually, I will use the sample code in Figure 10.3 to illustrate basic usage of the Config4JMS API. Then afterwards, I will discuss any remaining operations not illustrated by the example.

Figure 10.3: Example Use of Config4JMS
HashMap            cfgPresets = new HashMap();
Config4JMS         jms = null;
MessageProducer    producer = null;
TextMessage        msg = null;

//--------
// Initialisation
//--------
try {
    cfgPresets.put("username", "Fred");
    cfgPresets.put("password", "123456");
    jms = Config4JMS.create("example.cfg", "chat", cfgPresets,
                new String[] {"MessageConsumer", "chatConsumer",
                              "MessageProducer", "chatProducer",
                              "TextMessage",     "chatMessage"});
    jms.createJMSObjects();
    jms.getMessageConsumer("chatConsumer").setMessageListener(this);
    producer = jms.getMessageProducer("chatProducer");
    jms.startConnections();
} catch(JMSException ex) {
    System.err.println(ex.toString());
    try {
        if (jms != null) { jms.closeConnections(); }
    } catch(JMSException ex) {
        System.err.println(ex.toString());
    }
    System.exit(1);
}

//--------
// Sample producer code
//--------
msg = (TextMessage)jms.createMessage("chatMessage");
msg.setText("Hello, World");
producer.send(msg);

10.3.1  Basic Usage

The code in Figure 10.3 is an extract from the Chat.java sample application (in the samples/chat sub-directory of Config4JMS).

The code populates a HashMap with name=value pairs for a username and password. In the full Chat.java application, the HashMap is populated via command-line options.

Then the Config4JMS.create() factory operation is invoked to create a Config4JMS object. The first two parameters to this factory operation specify a configuration file (such as that shown in Figure 10.1) and a scope within that file. The third parameter is the HashMap containing name=value pairs. Config4JMS uses these to “preset” variables in a Configuration object before parsing the specified configuration file. In this way, these preset variables can override the default values of variables initialised with the conditional assignment operator ("?=") in the configuration file. The last (and optional) parameter to create() is an array of pairs of strings. Each pair specifies the type and name of an object that is expected to be specified in the configuration file. In effect, this parameter specifies a contract between the configuration file and the Java code. (If the contents of the configuration file do not satisfy the contract, then Config4JMS.create() throws an exception.) This contract enables a Java developer to not have to continually refer back to a configuration file to verify the types and names of the JMS objects being used in the Java code.

The create() operation parses the specified configuration file, performs schema validation on the specified scope, and ensures that the configuration file defines the expected objects of the specified type and name. The create() operation also copies the configuration information into a more convenient internal format.

The createJMSObjects() operation instructs Config4JMS to create all the objects defined in the configuration file.

After createJMSObjects() has been invoked, an application can call get<Type>() operations to retrieve specific objects by name. For example, getMessageConsumer("chatConsumer") returns the chatConsumer object of type MessageConsumer.

The startConnections() operation instructs Config4JMS to invoke the start() operation on all the Connection objects that it created from information in the configuration file. Likewise, closeConnections() causes close() to be invoked on all the Connection objects.

Each time createMessage() is called, it creates a new Message object. It sets headers specified in configuration (such as JMSExpiration and JMSPriority) and can also set type-specific name=value pairs as indicated in the properties configuration variable.

10.3.2  Other Operations

The example in Figure 10.3 illustrates most of the commonly-used operations provided by Config4JMS. I now quickly summarise its remaining operations.

Calling applyMessageProperties() on an existing Message object resets the object’s headers and properties to values specified in the named configuration scope.

Each list<Type>Names() operation returns an array of the names of objects of the specified type. For example, when using the configuration file shown in Figure 10.1, the listSessionNames() operation would return a list containing two strings: "prodSession" and "consSession".

The getConfiguration() operation returns a reference to the Config4* Configuration object used internally by the Config4JMS object.

The setExceptionListener() operation instructs Config4JMS to register an ExceptionListener object on all the Connection objects created from information in the configuration file.

The isNoConnection() operation examines a JMSException to determine if it was caused by a broken socket connection. A developer might use this operation in combination with setExceptionListener() to write an application that can detect when its connection to JMS infrastructure is broken, and attempt to re-establish the connection. The ReconnectableChat.java sample application (in the samples/chat sub-directory of Config4JMS) illustrates this technique.

The importObjectFromJNDI() operation uses the specified path to lookup an object from the naming service that was configured via the jndiEnvironment configuration variable.

The importObjectFromFile() operation reads a serialised Java object from the specified fileName.

10.4  Accessing Proprietary Features

Config4JMS is an abstract base class. Its static create() operation uses reflection to instantiate a concrete subclass. The name of the concrete subclass is specified by the config4jmsClass variable in the runtime configuration file. The example configuration file shown in Figure 10.1 sets that variable as follows:

config4jmsClass = "org.config4jms.portable.Config4JMS";

That class has been coded to recognise only configuration entries that reflect the standardised API of JMS. I have implemented another class that recognises the standard API plus proprietary enhancements of the SonicMQ implementation of JMS. You can use that class with the following setting:

config4jmsClass = "org.config4jms.sonicmq.Config4JMS";

I do not have any experience of other JMS products, but I imagine it should be possible to write additional subclasses of Config4JMS that support their proprietary enhancements.

If the value of config4jmsClass specifies the SonicMQ-specific class, then you can use SonicMQ-proprietary features in a straightforward manner, as I now discuss.

You can create instances of SonicMQ-proprietary types by creating appropriate configuration scopes:

MultiTopic.myTopic { ... }
XMLMessage.myMessage { ... }

You can set SonicMQ-proprietary properties in the same manner that you set JMS-standardised properties:

MessageConsumer.chatConsumer {
    createdBy = "consSession";
    create { ... }
    prefetchCount = "5";      # proprietary to SonicMQ
    prefetchThreshold = "2";  # proprietary to SonicMQ
}

The configuration file in Figure 10.1 retrieves administered objects from the naming service by setting obtainMethod to a value of the form "jndi#...", as I repeat below for ease of reference:

Topic.chatTopic {
    obtainMethod = "jndi#SampleTopic1";
}
ConnectionFactory.factory1 {
    obtainMethod = "jndi#SampleConnectionFactory1";
}

Within a ConnectionFactory sub-scope, if you set obtainMethod to the value "create", then you can create a ConnectionFactory using a proprietary constructor and use properties to specify a proprietary quality of service for it:

ConnectionFactory.factory1 {
    obtainMethod = "create";
    create {                      # parameters to constructor
        brokerURL = "...";
        defaultUserName = "...";
        defaultPassword = "...";
    }
    faultTolerant = "true;        # proprietary property
    flowToDisk = "ON";            # proprietary property
}

Within a Topic or Queue sub-scope, setting obtainMethod to the value "create" enables you to create a topic or queue by invoking a factory operation on a Session object:

Topic.chatTopic {
    obtainMethod = "create";
    createdBy = "prodSession";
    create {                      # parameters to constructor
        topicName = "...";
    }
}

The JMS specification defines factory operations on Session for creating destination object. However, the JMS specification does not define allowable values for the queueName or topicName parameter; such values are vendor-proprietary, which is why using factory operations to create destination objects is considered proprietary.

10.5  Benefits

Config4JMS offers several significant benefits, most of which are due to it enforcing a separation of concerns: it separates the initialisation of JMS from the “business logic” code that uses JMS.

10.5.1  Code Readability

As I explained in Section 9.4.2, the initialisation of JMS is verbose enough to confuse developers who are new to JMS. By encapsulating the initialisation steps in a configuration file, a new developer can write that once, forget about it, and then focus on the “business logic” code in a Java file.

For example, in the source code of Figure 10.3, the programmer need be concerned with just three JMS objects: a TextMessage, MessageConsumer and a MessageProducer. The other six JMS-related objects (a naming service, Topic, ConnectionFactory, Connection, and two Session objects) are really just “initialisation baggage” that has been encapsulated by Config4JMS.

The impact on code readability of encapsulating “initialisation baggage” can be dramatic. For example, the Chat.java demo supplied with Config4JMS contains significantly less code and is easier to understand than an equivalent demos written using the raw API of JMS.

10.5.2  Configurability

A lot of JMS behaviour is determined by qualities of service, for example, timeout values and whether messages are persistent. All these qualities of service can be expressed in a Config4JMS configuration file, which means that a Config4JMS-based application is highly configurable by default.

In contrast, if an application uses just the raw API of JMS, then its developer must explicitly write extra code to: (1) retrieve qualities of service information from a runtime configuration file, and (2) invoke the appropriate set<Name>() operations to apply them. If a developer is too busy or lazy to write such code, then the application will provide a hard-coded rather than configurable quality of service.

10.5.3  A Portable Way to Use Proprietary Features

Consider the following scenario. A producer application does not send any messages for an extended period of time, but then it sends a burst of, say, a hundred large messages, before going silent again. Such bursts of messages might cause a backlog of traffic to be sent via JMS, and this backlog might cause communications between applications to slow down while the backlog of messages is being transmitted.

The JMS specification does not offer any help to deal with a potential slowdown caused by a burst of many messages. However, some JMS products provide proprietary enhancements for dealing with this. For example, the SonicMQ product, provides a flow control feature that can throttle back the rate of message flow from a producer, and a separate flow to disk feature that can be used by consumer applications that cannot process a sudden burst of messages fast enough.

If you are using SonicMQ when developing a JMS application, then you might be be tempted to make use of the flow control and flow to disk features. Unfortunately, hard-coding use of these proprietary features into the Java code of your application would make your application non-portable to other JMS products. But if you use Config4JMS, then you can specify the use of these features in a configuration file. In this way, your Java code remains portable. If you later migrate to another JMS product, then you need only modify the configuration file to remove use of the SonicMQ-proprietary features and, optionally, make use of features proprietary to the replacement JMS product.

10.5.4  Reusability of Demonstration Applications

A JMS product might contain a dozen or more demonstration applications. One application is hard-coded to demonstrate communication via queues. A second application is hard-coded to demonstrate communication via topics. A third application is hard-coded to demonstrate the use of durable subscribers with topics. Several more applications are hard-coded the demonstrate the use of JMS-compliant qualities of service (using a separate application to demonstrate each quality of service). Yet more applications are hard-coded to demonstrate the use of vendor-proprietary qualities of service. And so on.

By using Config4JMS, a JMS vendor can significantly reduce the number of demonstration applications that need to be provided with a product. For example, the Chat.java application supplied with Config4JMS can be used to demonstrate: (1) communication via queues; (2) communication via topics; (3) the use of durable subscribers with topics; (4) different JMS-compliant qualities of service; (5) different vendor-proprietary qualities of service. All that is needed is to modify the application’s Config4JMS configuration file to specify the desired type of communication and the desired qualities of service.

Shipping a JMS product with a small number of highly configurable Config4JMS-based demonstration applications offers several benefits.

First, it benefits the JMS vendor because fewer demonstration applications have to be written, maintained and documented.

Second, it can shorten the learning curve for developers who are new to JMS or new to the proprietary features of a JMS product. This is because the develops can “play with” JMS concepts and proprietary features without having to write any code—instead, they just edit a configuration file. This shortening of the learning curve benefits not just developers employed by customers, but also newly employed technical staff of the JMS vendor.

Third, a Config4JMS-enabled demonstration application can be configured to obtain Destination and ConnectionFactory objects: (1) by invoking proprietary factory operations; or (2) from a pre-populated naming service. A developer who is learning to how use a JMS product can use technique (1) initially, and then switch to (2) later after learning how to do the required administration tasks. This means that a developer does not need to learn administration skills before being able to write a portable JMS application.

Finally, if a customer discovers a bug in a JMS product, then Config4JMS makes it easier for the customer to submit a test case. This is because a test case is likely to consist of a small amount of Java code (perhaps one of the demonstration applications supplied with the JMS product), plus a configuration file. In fact, the adaptive configuration capabilities Config4* might sometimes make it possible for a single configuration file to demonstrate a bug and a workaround for it. This possibility is illustrated by the configuration file shown in Figure 10.4.

Figure 10.4: Flexible test-case configuration file
workAroundBug ?= "false"; 
chat {
    config4jmsClass = "org.config4jms.acme.Config4JMS";
    jndiEnvironment = [ ... ];
    Topic.chatTopic { ... }
    ConnectionFactory.factory1 { ... }
    Connection.connection1 { ... }
    Session.prodSession {
        createdBy = "connection1";
        create {
            transacted = "false";
            acknowledgeMode = "AUTO_ACKNOWLEDGE";
        }
        @if (.workAroundBug == "true") {
            ... # set a proprietary property to one value
        } @else {
            ... # set the proprietary property to another value
        }
    }
    Session.consSession { ... }
    TextMessage.chatMessage { ... }
    MessageProducer.chatProducer { ... }
    MessageConsumer.chatConsumer { ... }
}

Let’s assume that, using that configuration file, the buggy behaviour can be illustrated by running the following command:

java TestCase -cfg test-case.cfg -scope chat

Then, by using a command-line option to “preset” the workAroundBug variable to true, the workaround for the bug can be illustrated:

java TestCase -cfg test-case.cfg -scope chat -set workAroundBug true

10.6  Drawbacks

Config4JMS has only a few, relatively minor drawbacks. I discuss them here for the sake of completeness.

10.6.1  Only Two Implementations So Far

To date, there are only two implementations of Config4JMS: one that provides access to only the portable API of JMS; and another implementation that provides access to that portable API plus the proprietary features of the SonicMQ product. It would be good to see Config4JMS enhanced to provide additional implementations that support the proprietary features of other JMS vendor products.

I am not aware of any technical issues that might make it difficult to enhance Config4JMS in this way. I estimate that a person who is already knowledgeable about a particular JMS product could extend Config4JMS to support its proprietary features with a few days of effort (at most).

10.6.2  Lack of Support for Legacy API

Currently, Config4JMS supports only the unified API of JMS 1.1. It should be easy to add support for the legacy API of JMS 1.0, if the need ever arises. However, I view the lack of support for the legacy API as being a benefit because, as I explained in Section 9.4.1, there are several drawbacks (and no benefits) associated with use of the legacy API.

10.7  Summary

In this chapter, I have explained how Config4JMS hides a lot of that complexity of JMS. The syntax of a Config4JMS configuration file is straightforward and the API is easy to use. Despite this simplicity, Config4JMS offers some significant benefits.

In the previous chapter, I explained how one variant of the 80/20 Principle applied to JMS: 80% of a product’s complexity is in 20% of its functionality. Config4JMS hides most of that complexity.


Previous Up Next