Mar 1, 2000 12:00 AM PT

Server-side Java: Patterns for flexible initialization, Part 1

A reflective approach to initializing your server-side system

Your Java server is almost complete: you've tested the code thoroughly, and everything seems to be running smoothly. It's time to wrap up your project and take care of the small details required to ship the code. Usually, one of those small details is creating and utilizing an application configuration file to store all the data relevant to the system setup. The code has to ship within an hour, so you hack something together that reads the configuration file and interprets its settings. A year later when you look back at the code, the sound of silent weeping starts to emanate from the depths of your professional programming soul.

Creating a simple but reusable process for reading and interpreting configuration information requires some thought. While preserving Java's simplicity, the process must be specialized enough to handle most steps in the application setup. Briefly, the setup/startup process can be described as the creation of at least one object of a class given in the configuration file, with constructor parameter values provided within the file.

Figure 1. The setup process flow

The two process steps, illustrated with the green arrows in Figure 1, are what you must implement to instantiate the startup object from the parameters in the configuration file. Thus, you need to implement the two process steps in a general fashion. Your goal is to be able to retain the setup process or pattern in future releases of your application software. Simply create a class (or upgrade an existing one) that should be used as the application initialization class. Such a class has only one purpose: to set up the application and to launch any side processes required to set the application server in standard operating mode.

Figure 2. The setup process flow and components

The most general entity in Java tends to be the interface, which can be implemented in a variety of ways without breaking the initial system design. Striving for generality in software components, the ConfigFileParser and ObjectFactory steps shown in Figure 2 could be Java interfaces and therefore implemented in myriad ways. Of course, this design would allow the setup subapplication to be as general as possible; if you suddenly feel an urge to change the format of the configuration file (say, to XML, CHTML, or some equally buzzword-compliant form of notation), just create a new ConfigFileParser implementation and plug it in to the application.

Now that the main setup tasks are defined, let's take a look at one possible system design. The overall design could, of course, be enhanced to accommodate powerful features. But for the purposes of this article, it's important to study the process as a whole, as well as study the collaboration between the ConfigFileParser and the ObjectFactory. Let's take a look at the entire subsystem illustrated in Figure 3, since it is fairly small:

Figure 3. The partial class diagram of the setup system

Systems that perform behind-the-scenes setup tasks should hide as much of the underlying structure as possible from the ultimate caller. By encapsulating most of the structure, few limitations need to be imposed on the final (and evolving) setup system. Thus, the only method that must be called to initialize the entire system is the doSetup() method of the Startup class. The process described above is then executed in the doSetup() method, as shown in Figure 4:

Figure 4. The doSetup() sequence diagram

The parseConfigFile() method is essentially a small parser whose algorithm could be exchanged at any point to handle different configuration file types and structures. By delegating the task of interpreting the information in the configuration file to another class, the system as a whole becomes more robust and may better withstand changes in its environment (the configuration file).

Let's take a look at a simple ConfigFileParser that handles old-style Java property configuration files:

Listing 1. SimpleConfigParser.java

// Copyright (c) 2000 jguru.com
// All rights reserved.
package com.jguru.initHandler;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.BufferedReader;
/**
 * Implementation of the ConfigFileParser interface that reads and parses
 * old-style Java Properties files.
 *
 * @author Lennart Jörelid, jGuru Europe
 * @version $Id$
 * @since January 2000
 */
public class PropertyConfigParser implements ConfigFileParser
{
  /**
    * Parses an old-style Java configuration file, constructed of
    * key=value pairs. The result is packaged in a map.
    *
    * Lines starting with "#" or " " (white space) characters are
considered
    * comments, and are disregarded by this parser method.
    *
    * @param absFilePath The absolute file path of the config file.
    * @return All parameters in the config file in a parsed format.
    */
   public Map parseConfigFile(String absFilePath) throws StartupException
   {
       // Check sanity
       File configFile = new File(absFilePath);(!configFile.exists() || !configFile.canRead())
       throw new StartupException("[parseConfigFile]: Config file '
" + absFilePath
         + "' nonexistent or unreadable.");
       // Open a Reader to the config file
       BufferedReader in = null;
       try
       {
          in = new BufferedReader(new FileReader(configFile));       catch(FileNotFoundException ex)
       {
          // We should never end up here, since
          // we already checked that the readable config file existed...
          throw new StartupException("[parseConfigFile]: Guru
meditation; Config file '"
                        + absFilePath + "' maybe not found.");
       }
       // Create the resulting map and populate it
       // with the entries in the configuration file.
       Map aMap = Collections.synchronizedMap(new HashMap());
       // Read all lines in the config file
       while(true)           try
           {
              String aLine = in.readLine();
              if (aLine == null) break;
              // Disregard comments that reside on lines starting
              // with white space or a pound sign.
              if(! aLine.startsWith("#") || ! aLine.startsWith
(" "))
              {
                  // This is a config directive -- not a comment.
                  // Split the line/string at the index of the "
=".
                  int divisor = aLine.indexOf("=");
                  if (divisor != -1)
                  {
                      // Create a new entry in the return map
                      aMap.put(
                           aLine.substring(0, divisor),                 //
The key
                           aLine.substring(divisor + 1, aLine.length()) //
The value
                           );
                  }
              }
           }
           catch(IOException ex)
           {
              throw new StartupException("[parseConfigFile]: " +
ex);
           }
       }
       return aMap;
   }
}

The above listing is a very simple parser method; it simply reads through all the lines in the configuration file, discarding the lines starting with "#" or " " (white space) and treating all other lines as configuration directives. The results are returned in the form of a map, which the ObjectFactory can use to automate the creation of the startup objects.

Of course, the ConfigFileParser implementation is written for one particular configuration file syntax. Note that the sample configuration file below has a very rudimentary syntax in which the system root class is com.jguru.initHandler.TestClass and all the arguments to that class's constructor are provided as a pair of two keys: the constructor parameter [argX] and its corresponding type [typeX].

Listing 2. Sample configuration file

############################################
#                                          #
# jGuru Sample Configuration file          #
#                                          #
############################################
startupClass=com.jguru.initHandler.TestClass
arg0=A custom config string
type0=java.lang.String
arg1=42
type1=java.lang.Integer

Thus, the equivalent Java statement that you should extract from the configuration file above is new com.jguru.initHandler.TestClass("A custom config string", 42); calling the constructor public com.jguru.initHandler.TestClass(String, int);. The ObjectFactory is responsible for manufacturing the object in this fashion.

The ObjectFactory component

The engineering delicacy of that example lies solely in the

ObjectFactory

method implementation, with which the

Startup

class may instantiate a root object from any class. This is an important feature of the startup system. If your system structure should be altered in the future, you need only implement a new root class and modify the configuration file to completely revamp the system. This is an example of the usefulness of the Abstract Factory pattern (see

Design Patterns,

by Erich Gamma et al. in

Resources

). The only relevant method of the

ObjectFactory

,

createObject

performs five tasks (listed below) marked

### 1)

-

### 5)

, as shown in the

ObjectFactory.java

code listing.

  1. Load the class supplied as a string argument using the Class.forName() method.
  2. Use reflection to find all the constructors of the class loaded in (1).
  3. Use reflection to check the type and number of arguments for each constructor.
  4. If the type and number of arguments provided as a parameter to the createObject match those of the current constructor from (3), mark the constructor as an invocation candidate. If not, move to the next declared constructor and try again.
  5. Instantiate an object of the class from (1) using the invocation candidate from (4).

Listing 3. ObjectFactory.java

// Copyright (c) 2000 jguru.com

// All rights reserved.

package com.jguru.initHandler;

import java.lang.reflect.Constructor;

/**

 * Implementation of the ObjectFactory interface that

 * creates instances of a given class using provided

 * argument parameters.

 *

 * @author Lennart Jörelid, jGuru Europe

 * @version $Id$

 * @since January 2000

 */

public class SimpleObjectFactory implements ObjectFactory

{

    /**

     * Instantiates an object from the class given and with the

     * provided arguments. Primitive arguments must be sent in

     * their respective java.lang containers. (i.e., java.lang.Byte

     * for a byte argument, java.lang.Integer for an int argument, etc.)

     * <br>

     * All exceptions thrown within the createObject method are

     * type-shifted into a StartupException.

     *

     * @param className The fully qualified name of the class from which

     * an object should be created.

     * @param args The argument array to be fed to the constructor.

     * @return The fully instantiated object.

     * @exception com.jguru.initHandler.StartupException thrown if

     * anything goes wrong in creating the object.

     */

    public Object createObject(String className, Object args[])

    throws StartupException

    {

       // The provided class and the constructor that should

       // ultimately be invoked to create an object of the class.

       Class theClass = null;

       Constructor invocationCandidate = null;

       Object toReturn = null;

       // ### 1) Load the class and check sanity

       try

       {

           theClass = Class.forName(className); catch(ClassNotFoundException ex)

       {

           throw new StartupException("[Startup createObject]: Could

not load class " + className);

       }

       // ### 2) Get the constructors of the declared class.

       Constructor[] theConstructors = theClass.getConstructors();

       // ### 3) Check number and type of arguments required for each

       // found constructor in the loaded class.

outer: for(int i = 0; i < theConstructors.length; i++)

       {

           // Get the class of the arguments

           Class[] constructorArgs = theConstructors[i].getParameterTypes

();

           // If we have different argument lengths,

           // the found constructor could not be called using the

           // current parameter array. Skip to the next constructor.

           if(args.length != constructorArgs.length)

           continue outer;

           // Is this a no-argument constructor?

           if(args.length == 0)

           {

               invocationCandidate = theConstructors[i];break outer;

           }

           // ### 4) This constructor requires parameters.

           // Loop through all provided parameters to verify

           // that they match the required ones.

           for(int j = 0; j < args.length; j++)

           {

               // Check if the constructor argument might be

               // typecast into the proper class. This must be done for all

               // primitive types, which must be encapsulated in their

java.lang.XXXX

               // containers to be passed as part of an object[] array.

               boolean matched = false;

               if(constructorArgs[j].isPrimitive())

               {

                  String primName = constructorArgs[j].getName();

                  String argName = args[j].getClass().getName();

                  if(primName.equals("byte") &&

argName.equals("java.lang.Byte")) matched = true;

                  if(primName.equals("short") &&

argName.equals("java.lang.Short")) matched = true;

                  if(primName.equals("int") &&

argName.equals("java.lang.Integer")) matched = true;

                  if(primName.equals("long") &&

argName.equals("java.lang.Long")) matched = true;

                  if(primName.equals("long") &&

argName.equals("java.lang.Integer")) matched = true;

                  if(primName.equals("float") &&

argName.equals("java.lang.Float")) matched = true;

                  if(primName.equals("double") &&

argName.equals("java.lang.Double")) matched = true;

                  if(primName.equals("char") &&

argName.equals("java.lang.Character")) matched = true;

                  if(primName.equals("boolean") &&

argName.equals("java.lang.Boolean")) matched = true;

                  if(!matched) continue outer;

               }

               if( !matched && (!args[j].getClass

().equals(constructorArgs[j]))) }

           // We found the constructor to invoke.

           // Check sanity, and assign the value to

           // the invocationCandidate variable.

           if(invocationCandidate != null)

           {

               // We should never wind up here....

               throw new StartupException("[Startup createObject]:

Guru meditation; Found 2 " +

                     + "constructors with same signature in class

" + theClass.getName());

           }

           // We found an invocation candidate, whose number and type of

arguments

           // match the argument array provided to this method.

           // Assign the constructor to the invocationCandidate variable.

           invocationCandidate = theConstructors[i];

       // Check sanity

       if(invocationCandidate == null)

       throw new StartupException("[Startup createObject]: Found no

constructor matching argument " +

             + " criteria in " + theClass.getName());

       // ### 5) Sane. Invoke the constructor and return the object.

       try

       {

          if(args.length == 0)

          {

             toReturn = theClass.newInstance();

          }

          else

          {

           toReturn = invocationCandidate.newInstance(args); }

       catch(Exception ex)

       {

          // Despite all the testing, something was quite wrong here.

          // Throw the proper exception from this method.

          throw new StartupException("[Startup createObject]:

Invocation failed for object of type "

                           + className + "(" + theClass.getName()

+ "), Exception: "

                           + ex.getClass().getName() + " " +

ex.getMessage());

       }

       // All done. Return.

       return toReturn;

   }

    public Object createObject(String className)

    {

       return createObject(className, new Object[]{});

    }

}

Tying the knot

The final structure of the minimal setup subsystem includes a custom Exception class. It differentiates runtime exceptions that arise in the startup process from other runtime exceptions. The subsystem also includes TestClass, which is the application system's singleton root node. Note that the TestClass class provides five constructors. The one being called should be identified by the object factory using introspection. The main point of this small setup subsystem is that the implementation and type of the root-node application class is completely unknown to the Startup, ObjectFactory, and ConfigFileParser implementations. The configuration file is the only place in the system that contains information regarding which root-node class should be instantiated. Thus, the setup subsystem has completely decoupled the implementation and type of the root-node class from the implementation of the setup subsystem itself. Figure 5 shows the class diagram for the setup system:

Figure 5. The setup system class diagram

Let us finally take a look at the TestClass and Startup implementations.

Listing 4. TestClass.java

// Copyright (c) 1999 jguru.com

// All rights reserved.

package com.jguru.initHandler;

public class TestClass

{

  private String message;

  public TestClass()

  {

     this.message = "Default constructor called";

  }

  public TestClass(String message)

  {

    this.message = "TestClass(String) called with message: " + message;

  }

  public TestClass(int anInt)

  {

    this.message = "TestClass(int) called with number: " + anInt;

  }

  public TestClass(String message, int anInt)

  {

    this.message = "TestClass(String, int) called with parameters: ("

    + message + ", " + anInt + ")";

  }

  public TestClass(String message, short aShort)

  {

    this.message = "TestClass(String, short) called with parameters: ("

    + message + ", " + aShort + ")";

  }

  public String toString()

  {

     return this.message;

  }

}

Listing 5. Startup.java

// All rights reserved.
package com.jguru.initHandler;
import java.util.Map;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
/**
 * Driver class that starts up the system.
 *
 * @author Lennart Jörelid, jGuru Europe
 * @version $Id$
 * @since January 2000
 */
public class Startup
{
    /** The ObjectFactory used to create the system root instance. */
    private ObjectFactory    objFactory;
    /** The ConfigFileParser used to read and interpret the Config file. */
    private ConfigFileParser confParser;
    /** Absolute path of the config file. */
    private String           configFilePath;
    /**
     * Creates a new startup object, using an old-style Java property
     * file for storing configuration properties and a simple
     * object factory for creating objects.
     */
    public Startup(String configFilePath)
    {
        // Create internal state objects
        this.confParser = new PropertyConfigParser();
        this.objFactory = new SimpleObjectFactory();
        this.configFilePath = configFilePath;
    }
    /**
     * Main setup entry method that reads the configuration file,
     * interprets all directives, and creates the launcher
     * dynamic object that could start up the entire application.
     */
    public Object doSetup() throws StartupException
    {
       // Read all directives from the config file
       Map configDirectives = confParser.parseConfigFile(configFilePath);       // Find the startup class to load
       String startupClassName = (String) configDirectives.get
("startupClass");
       // Find the number of arguments and create the object array
       // where all the parameters go.
       int numArgs = 0;
       Object[] params = null;
       for(int i = 0; configDirectives.containsKey("type" + i); i++)
numArgs++;
       params = new Object[numArgs]; for(int j = 0; j < numArgs; j++)
       {
           // Find the type and value of the current argument
           String theType     = (String) configDirectives.get("type" + j);
           String theArgument = (String) configDirectives.get("arg" + j);
           params[j] = objFactory.createObject(theType, new Object
[]{theArgument});
       // Create the startup object
       Object startupObject =
this.objFactory.createObject(startupClassName, params);
       return startupObject;
    }
    public static void main(String[] args)
    {
       Startup launcher = new Startup("/setupApp/settings.cfg");
       Object dynObject = launcher.doSetup();
       System.out.println("Created instance of " + dynObject.getClass
().getName());
       System.out.println("[Instance]: " + dynObject);
    }
}

Conclusion

When developing a setup subsystem, strive to decouple the application system root class from the implementation of the setup subsystem. The less tightly coupled the two are, the lower the system maintenance cost. The reflectance classes of the Java API greatly facilitate the introspection required to implement such a decoupling. Use a reflective approach when designing your setup subsystem, and its overall maintenance cost will reduce dramatically. In a next issue of JavaWorld, I will augment the small setup subsystem by working with some of the other reflectance classes of the Java API. Stay tuned!

Lennart Jorelid is a server-side Java and ecommerce content expert working for jGuru Europe. With working experience that spans projects in the United States, Canada, United Kingdom, Switzerland, Sweden, and Germany, Lennart is a recognized expert, architect, and educator in the Java technology community. Based in Göteborg, Sweden, he is writing a book on server-side Java patterns. Lennart is a fan of skiing, acting, and science fiction. Learn more about Lennart at jGuru.com. JavaWorld and jGuru have formed a partnership to help the community better understand server-side Java technology. Together, JavaWorld and jGuru are producing articles and free educational Web events.

Learn more about this topic

Server-side Java: Read the whole series -archived on JavaWorld