Previous Up Next

Chapter 11  Architecture of Config4JMS

11.1  Introduction

In this chapter, I discuss how Config4JMS makes effective use of Config4* and its schema language. However, to help readers understand why Config4JMS uses Config4* in the way it does, I first need to provide an overview of the architecture of Config4JMS.

11.2  Packages

The source code of Config4JMS is spread over four packages:

org.config4jms
org.config4jms.base
org.config4jms.portable
org.config4jms.sonicmq

The org.config4jms package contains just two classes. One of these, Config4JMS, is an abstract base class that defines the API of Config4JMS. The other class, Config4JMSException.java, inherits from JMSException.

The org.config4jms.base package contains some basic functionality that is used by the classes in both the portable and sonicmq packages.

The org.config4jms.portable package contains a concrete subclass of Config4JMS, plus supporting classes. The concrete subclass, which is also called Config4JMS, supports the standardised API of JMS.

The org.config4jms.sonicmq package contains a concrete subclass of Config4JMS, plus supporting classes. This concrete subclass, which is also called Config4JMS, supports the standardised API of JMS plus proprietary enhancements that are specific to the SonicMQ implementation of JMS.

I considered having the classes in the sonicmq package inherit from their counterparts in the portable package. However, I decided against this approach because I felt it might result in the anti-pattern known as the yo-yo problem. Instead, I felt it was better (or, at least, less bad) to employ the “code reuse by copy-and-pasting” anti-pattern. If you wish to extend Config4JMS to support another implementation of JMS called, say, Foo, then you can do this by creating a package called org.config4jms.foo, copying all the files from the portable package into this new package, and then modifying the copied files to add support for Foo-proprietary features.

11.3  Important Classes

Abridged details of three important classes in the org.config4jms.base package are shown in Figure 11.1. I will discuss each of the three classes in turn.

Figure 11.1: Important classes in the org.config4jms.base package
public abstract class Info
{
    public abstract void validateConfiguration()
                                    throws Config4JMSException;
    public abstract void createJMSObject()
                                    throws Config4JMSException;
    public abstract Object getObject();
    ... // other operations omitted for brevity
}

public class TypeDefinition
{
    public TypeDefinition(
             String      typeName,
             String[]    ancestorTypeNames,
             String      className);
    ... // operations omitted for brevity
}

public class TypesAndObjects
{
    private TypeDefinition[]  typeDefinitions;
    private HashMap           objects;

    public void validateConfiguration(
                       Config4JMS  config4jms,
                       String      scope) throws Config4JMSException;
    ... // other operations omitted for brevity
}

11.3.1  The Info Class

There is a subclass of Info for each JMS-related type. For example, the ConnectionInfo class is for the JMS Connection type, and the SessionInfo class is for the JMS Session type. One entire set of subclasses of Info are defined in the portable package. Another entire set of subclasses of Info are defined in the sonicmq packages.

The configuration file in Figure 10.1 defines may scopes for JMS objects. Config4JMS creates an instance of the appropriate Info subclass for each of those scopes. For example, Config4JMS creates a ConnectionInfo object for the Connection.connection1 scope, and creates two SessionInfo objects: one for the Session.prodSession scope, and another for the Session.consSession scope.

A concrete subclass of Info implements its operations as follows.

11.3.2  The TypeDefinition Class

The Config4JMS class is not hard-coded with knowledge of the numerous subclass of Info. Instead, Config4JMS uses Java reflection to create and manipulate Info objects from metadata.1 This metadata is provided by instances of the TypeDefinition class (see Figure 11.1). I will illustrate this with three examples.

The TypeDefinition object below indicates that a Session (for example, a Session.prodSession scope) should be processed by creating an instance of the org.config4jms.portable.SessionInfo class. The null value indicates that Session is a base type, that is, it does not have any ancestor types:

new TypeDefinition("Session", null,
                   "org.config4jms.portable.SessionInfo");

This next TypeDefinition object indicates that Topic is a subtype of Destination, and an instance of Topic should be processed by creating an instance of the org.config4jms.portable.TopicInfo class:

new TypeDefinition("Topic", new String[]{"Destination"},
                   "org.config4jms.portable.TopicInfo");

This final example indicates that Destination has neither ancestor types nor an implementation class. In effect, it is an abstract base type:

new TypeDefinition("Destination", null, null);

The abstract nature of Destination means you cannot have Destination sub-scopes in a Config4JMS configuration file. However, an application can invoke, say, getDestination("chatTopic") on a Config4JMS object because Topic is a subtype of Destination. Likewise, invoking listDestinationNames() would return "chatTopic" among its results.

11.3.3  The TypesAndObjects Class

The TypesAndObjects class (see Figure 11.1) contains two instance variables:

private TypeDefinition[]  typeDefinitions;
private HashMap           objects;

The typeDefinitions array holds metadata for all JMS data types. The org.config4jms.portable.PortableTypesAndObjects class creates an array of TypeDefinition for all the standardised JMS types. Conversely, the org.config4jms.sonicmq.SonicMQTypesAndObjects class creates an array of TypeDefinition for all the standardised JMS types plus the SonicMQ-proprietary types.

The objects variable is a HashMap that provides a flexible way to retrieve an Info object. For example, a TopicInfo object created from the Topic.chatTopic scope is registered in the HashMap via three keys: "chatTopic", "Topic,chatTopic" and "Destination,chatTopic".

The TypesAndObjects class provides a lot of operations that manipulate its instance variables. In fact, a lot of Config4JMS functionality is implemented by having the Config4JMS class delegate to the TypesAndObjects class.

11.4  Algorithms Used in Config4JMS

With the knowledge of important infrastructure classes provided in Section 11.3, the implementation of Config4JMS is easy to understand.

11.4.1  Initialisation

Pseudocode for the initialisation of a Config4JMS object is shown in Figure 11.2.

Figure 11.2: Pseudocode implementation of Config4JMS.create()
package org.config4jms;
public abstract class Config4JMS
{
    public static Config4JMS create(
        String             cfgSource,
        String             scope,
        Map                cfgPresets) throws Config4JMSException
    {
        //--------
        // Parse the configuration file and retrieve the name of the
        // concrete subclass that we should create.
        //--------
        cfg = Configuration.create();
        ... // populate cfg with name=value pairs from cfgSource
        cfg.parse(cfgSource);
        className = cfg.lookupString(scope, "config4jmsClass",
                                "org.config4jms.portable.Config4JMS");

        //--------
        // Use reflection to create an instance of the specified class
        //--------
        c = Class.forName(className);
        cons = c.getConstructor(new Class[]
                                Configuration.class, String.class);
        return (Config4JMS)cons.newInstance(new Object[]cfg, scope);
    }

    protected Config4JMS(
        Configuration      cfg,
        String             scope,
        TypesAndObjects    typesAndObjects) throws Config4JMSException
    {
        this.cfg = cfg;
        this.scope = scope;
        this.typesAndObjects = typesAndObjects;
        naming = null;
        jndiEnvironment = cfg.lookup(scope, "jndiEnvironment",
                                     new String[0]);
        typesAndObjects.validateConfiguration(this, scope);
    }
    ...
}

The create() operation creates an empty Configuration object and copies all the name=value pairs from the cfgPresets variable into it. Then the configuration file is parsed, and lookupString() is invoked to get the value of the config4jmsClass variable. Finally, Java’s reflection capabilities are used to create an instance of the specified class, which must be a subclass of org.config4jms.Config4JMS.

The constructor of the subclass just invokes its parent class’s constructor, passing a TypesAndObjects parameter that provides the metadata necessary to create JMS-based objects via reflection.

The base class constructor, which is shown in Figure 11.2, initialises instance variables and then performs schema validation of the configuration file by invoking validateConfiguration() on its typesAndObjects object.

11.4.2  Schema Validation

TypesAndObjects.validateConfiguration() performs schema validation in three steps.

Step 1.
A schema for the top-level of the configuration scope is created. This schema is of the form:
        String schema = new String[] {
            "config4jmsClass = string",
            "jndiEnvironment = table[name,string, value,string]",
            "ConnectionFactory = scope",
            "Connection = scope",
            "Session = scope",
            ...
        };
        
Only the first two entries (config4jmsClass and jndiEnvironment) in the schema are hard-coded. The remaining schema entries are obtained by iterating over the typeDefinitions array, which holds metadata for all JMS data types. For each non-abstract data type in that array, a string of the form "<type-name> = scope" is added to the schema.

Once that schema definition has been created, it is used to validate the top-level configuration scope:

        sv = new SchemaValidator(schema);
        sv.validate(cfg, scope, "", false, Configuration.CFG_SCOPE_AND_VARS);
        

The false parameter indicates that the validation should not recurse into sub-scopes.

Step 2.
Each sub-scope corresponding to a JMS data type can contain nested scopes but not variables. For example, a Session scope can contain nested scopes (one for each session object), but it cannot contain any variables. The code below performs this validation check.
        schema = new String[0];
        sv = new SchemaValidator(schema);
        for (i = 0; i < typeDefinitions.length; i++) {
            typeDef = typeDefinitions[i];
            if (typeDef.getIsAbstract()) { continue; }
            typeName = typeDef.getTypeName();
            if (cfg.type(scope, typeName) == Configuration.CFG_NO_VALUE) {
                continue;
            }
            sv.validate(cfg, scope, typeName, false,
                        Configuration.CFG_VARIABLES);
        }
        
First an empty schema (that is, an array containing zero strings) is created. Then, when iterating over the typeDefinitions array, abstract types are ignored (because they cannot have a scope in the configuration file). A concrete type is also ignored if cfg.type() indicates there is no scope matching the type’s name in the configuration file. If there is a scope for the type, then it is validated, but only for the variables it might contain.
Step 3.
Nested for-loops are used to iterate over every type.name configuration sub-scope (for example, ConnectionFactory.factory1, Connection.connection1, Topic.chatTopic, Session.prodSession, Session.consSession and so on). For each such sub-scope, Java’s reflection capabilities are used to create an instance of a <type>Info object for that scope. For example, a TopicInfo object is created for the Topic.chatTopic scope. Each <type>Info object is registered in the objects map multiple times (as discussed in Section 11.3.3). Then, validateConfiguration() is invoked on the newly created <type>Info object so the object can validate (and cache in instance variables) the configuration variables in its own scope. The schema validation code within a <type>Info class is straightforward, as you will be able to see if you example the source code of any those classes.

The three-step algorithm could be simplified by combining steps 1 and 2 into a single step, as I now discuss.

Recall that step 1 creates the schema definition by iterating over the typeDefinitions array and, for each non-abstract data type in that array, adding a string of the form "<type-name> = scope" to the schema. That algorithm could be modified so that another string, this one of the form "@ignoreScopesIn <type-name>", is also added to the schema. The resulting schema would be of the form:

String schema = new String[] {
    "config4jmsClass = string",
    "jndiEnvironment = table[name,string, value,string]",
    "ConnectionFactory = scope",
    "@ignoreScopesIn ConnectionFactory",
    "Connection = scope",
    "@ignoreScopesIn Connection",
    "Session = scope",
    "@ignoreScopesIn Session",
    ...
};

Once that schema definition has been created, it would then be used to perform a recursive schema validation of the top-level configuration scope:

sv = new SchemaValidator(schema);
sv.validate(cfg, scope, "", true, Configuration.CFG_SCOPE_AND_VARS);

The true parameter indicates that the validation does recurse into sub-scopes.

Those changes to step 1 of the algorithm are quite trivial (they require adding one line of new code and modifying another line of existing code), and they eliminate the need for step 2 (which accounts for 12 lines of code). Some readers may be wondering why Config4JMS does not use the simpler algorithm that I just described. There are two reasons for this:

11.4.3  The createJMSObjects() Operation

Config4JMS.createJMSObjects() delegates to an identically-named operation on the TypesAndObjects class. That operation iterates over all the <type>Info> objects that had been created during step 3 of the schema validation algorithm, and invokes createJMSObject() on each object. The only complication is that JMS objects have to be created in a particular order. For example, a ConnectionFactory object must be created before it can be used to create a Connection object. Likewise, a Connection object must be created before creating Session objects.

The order-of-creation guarantee is provided in a simple way. When an array of TypeDefinition objects is being created (see Section 11.3.3), the order of elements in the array specifies the order in which objects of those types will be created. That enables createJMSObjects() to ensure that JMS objects are created in the required order.

11.5  Comparison with Spring

The purpose of Config4JMS overlaps a bit with that of the Spring framework. In particular, both use information in a configuration file to create Java objects. This overlap is bound to invite comparisons between the two projects. In reality, the two projects are more different than alike, so a comparison of them would be akin to comparing apples and oranges.

One obvious difference between Spring and Config4JMS is the configuration syntax used: Spring uses XML while Config4JMS uses Config4*. However, that difference is relatively unimportant.

A much more significant difference—and what I consider to be the primary difference—is that Spring can create Java objects of arbitrary types, while Config4JMS is restricted to creating only JMS objects. Many other differences between Spring and Config4JMS can be traced back to that primary difference. For example:

11.6  Future Maintenance

My inspiration for developing Config4JMS came about in early 2010, when my manager asked me to learn JMS. I began by reading the JMS specification and some product manuals. Unfortunately, the problems discussed in Section 9.4 made learning JMS more difficult than I had expected, so I decided to write Config4JMS. I figured that: (1) implementing this class library would help me to learn JMS; and (2) the resulting class library might actually be useful. I was right on both counts. Development of Config4JMS took about two weeks of hard but enjoyable work.

Unfortunately, a few few months after developing Config4JMS, I was laid off during a restructuring of the company I worked for. My new career plans mean it is unlikely that I will be working much with JMS in the future, so I will have little motivation to maintain and extend Config4JMS. If any readers would like take over maintenance of Config4JMS, then that would be great. Please let me know if you are interested in taking on this responsibility.

11.7  Summary

In this chapter, I have provided an overview of the architecture of Config4JMS. The “lots of power from a small class library” feel of Config4JMS is due to a synergy between its use of Config4J and Java’s reflection capabilities. This synergy enables Config4JMS to provide a useful simplification and portability wrapper around JMS with a relatively small amount of code.

The Config4J schema language is not powerful enough to validate an entire Config4JMS file in one go. However, Config4JMS works around this in a straightforward manner: as I discussed in Section 11.4.2, it breaks up schema validation into a sequence of smaller steps, each of which is within the capabilities of the schema language.


1
Readers not familiar with Java reflection can find an informative overview in a free training course: www.ciaranmchale.com/training-courses.html#training-java-reflection. A more detailed discussion of Java reflection can be found in an excellent book [FF05] upon which that training course is based.

Previous Up Next