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

Customizing command targets in your system initialization

After some pain and agony, your application system is now recoded to use the Setup pattern described in “Server-side patterns for flexible initialization, Part 1.” Although it certainly took some effort to dispose of the fairly specific setup structure you had implemented, all of it is now working smoothly. However, you are still two steps away from being able to launch the system in a general manner:

  1. You must still know the name of the method to call in the dynamically loaded object to start up the system. It may be properly configured, but you still must invoke the launch method in a general enough way. Thus, you need to choose a general approach and a pattern for invoking that method.
  2. There is currently no general runtime control over the various system modules; the system is presently — per definition — running in a normal operating state when you launch the startup method as described above. Most application systems need fine-grained runtime control and several operation states to function properly. You need, therefore, to implement a pattern that would provide a general system-state control.

When selecting any approach for achieving general runtime control in a large application system, you must modify existing code as little as possible. If it is done correctly, you should need to modify only one class per application system module to enable general centralized system-state control. The two patterns that most closely match the restraints imposed by the two conditions listed above are slightly modified versions of the Builder, Facade, and Bridge patterns. If you add these to the modified Abstract Factory pattern (discussed in Part 1), your setup system will reach the level of maturity needed for general application systems. The process flow should therefore be extended as shown in Figure 1:

Figure 1. Augmented setup process flow

The three major process steps are illustrated with the green arrows in Figure 1. You must implement classes corresponding to all these steps to instantiate the startup object and launch the system from the parameters in the configuration file. The two first components of the flow, ConfigFileParser and ObjectFactory (shown in Figure 2), were examined in some detail in Part 1. In this article, I will discuss the needs of the last setup process step, State, as well as the runtime management of application system state. The component handling the invocation and centralized management of system state is known as the system controller instance. This instance is described by a Java interface: SystemController, as shown in Figure 2:

Figure 2. Augmented setup process flow and components

The main reason for using interfaces to describe all process step handlers is their flexibility. Any process flow component may be reimplemented in any manner without breaking the initial system design. You have already seen that the ConfigFileParser and ObjectFactory steps are declared as Java interfaces, implemented by specific subclasses. Preserving the pattern, the SystemController is declared and implemented in the same manner. Since a runtime system controller is a fairly complex entity to implement, its substructure is compound enough to accommodate different levels of automation and system integration.

Figure 3. Setup system partial class diagram

I covered the leftmost hierarchy of the startup system of Figure 3 in Part 1. I will cover the rightmost SystemController interface and its associated classes in this segment. The ObjectFactory creates a single instance of a SystemController subclass (a concrete subclass of the AbstractSystemController) and invokes a method to create all system subcomponents. After all these subcomponents are created, you can use the SystemController interface to monitor and control the application system’s operation status. Most larger application systems enable their operation status to turn services on and off in a correct order when starting up or shutting down the system (or some parts of the system). For instance, the services of any Unix operating system are started in a controlled fashion when the operating system changes operation state (run level).

I recommend designing your system with the Interface pattern as much as possible, since any implementation may dynamically be switched for another without affecting other system modules. The overall design could, of course, be improved and boosted to accommodate even more powerful features — but for this article, the important steps are the complete process and module management as a whole and the collaboration between the interface implementations.

In Part 1, the collaboration diagram showing interaction between the Startup class and its ObjectFactory and ConfigFileParser utility classes consisted of two method calls.

The doSetup sequence diagram, shown in Figure 4, is now modified with an extra method call, which loads all system modules (subsystems) and puts the application system in normal state:

Figure 4. The doSetup() sequence diagram

What are application subsystems?

Despite the minimal modification to the collaboration diagram, it is quite difficult to add less than an extra method call. The modifications and additions to the class diagram and the implications for centralized application state control are far from insignificant, since they provide the application system with the concept of application states (run levels). A goal in providing the structure is, of course, that the existing application system should need as little modification to its existing structure as possible. I have generally found small application system modifications doable and major application system modifications a politically correct way of spelling recode or at least refactor, to appease upper management.

One of the major keys to aptly managing an application system transition is to delegate tasks to subsystems. In this context, a subsystem is a Facade pattern hiding or managing a multitude of other classes behind its method interface. The purpose of the subsystem facade is to reduce centralized management complexity by providing subsystem state information and handling state change requests from the SystemController. Interaction between the SystemController and each subsystem is greatly facilitated by a common public type, so an interface specification is the logical choice for a subsystem type. The subsystem facade is only an interface definition, which needs to be implemented by (a minimum of) one class per subsystem. Moving an entire application system to the centralized setup and state-handling architecture proposed in this article is therefore a relatively painless operation. If your application subsystems are managed in different ways today, the Bridge pattern can facilitate your retrofitting process. Indeed, such a retrofit essentially declares only the type and delegates its method calls to other sets of existing method calls within the subsystem.

Therefore, use subsystems within the application to facilitate centralized management of your whole system. The interface mechanism is a good way to define subsystems, since it is dissociated from any actual implementation and poses no real demands on the implementing class. Use a Delegator pattern to superpose the general subsystem module structure on your existing structure. The message delivery between modules and the module manager follow the Observer/Observable pattern for registration and event notification.

Next, I’ll go over a simple approach to creating a subsystem-type hierarchy, shown in Figure 5:

Figure 5. Augmented setup process flow hierarchy

The hierarchy above describes the three interfaces — SystemController, ModuleManager, and SystemModuleController (together with abstract implementations) — that are required to create a smaller system for module and system-state management. Although small and subject to much improvement, the application subsystem management structure proposed here is adequate for rather large systems. Note that the two subclasses at the bottom of the hierarchy are test implementations and should be regarded as examples only.

The purpose of each functionality definition interface is:

  • SystemController. This interface defines system status control methods, which brings the system to a specified operation level. Such operation levels correspond to different required system states, such as Halt, Maintenance, Normal, and so forth.
  • ModuleManager. This interface defines an API for managing application subsystem modules. All such managed modules may have their operation states modified centrally by the SystemController.
  • SystemModuleController. This interface defines the interface methods of an application subsystem facade object. The subsystem’s responsibility is to register itself with the application’s ModuleManager. After this registration, the SystemModuleController must respond to commands and queries from the SystemController regarding its operation state. Each subsystem manages its operation state according to its own needs.

Each abstract implementation is a system adapter, which functions in a manner similar to the event adapter classes of package java.awt.event. You may, without breaking the architecture, choose an implementation standard other than what was obtained from subclassing the abstract adapter implementations in the hierarchy. Implementation or subclassing may be chosen at different levels; if you choose to extend AbstractSystemController, no special requirements have to be met by the subsystem facade classes. Should you select the full power of the Setup pattern by subclassing the AbstractModuleManager, you must implement the SystemModuleController interface for each subsystem facade class. (Granted, this should not be a big task.)

Subsystems tend to depend on the services of other subsystems. For instance, a distributed object lookup service depends on the services of the network subsystem. Any subsystem module management system must make sure that all parent subsystems are brought to a specified higher state before bringing any dependent subsystems to that particular state. The opposite is true if the application system state change is reversed; all dependent modules must be brought down to a lower operation state before bringing down any parent modules. To ensure that no dependency problems remain during system-state shifts, all SystemModuleController objects should register their parent modules. When in need, the SystemController may therefore make certain that all dependencies are acknowledged during state shifts.

Let us take a look at the SystemModuleController interface to better understand a subsystem module.

Listing 1. SystemModuleController interface

// Copyright (c) 1999 jguru.com
// All rights reserved.
package com.jguru.initHandler;
import java.util.Iterator;
import java.util.List;
/**
 * Definition of the interface methods of an application subsystem
 * facade object. The responsibility of the subsystem is to register
 * itself with the application ModuleManager. After such registration,
 * the SystemModuleController must respond to commands and queries
 * from the <code>SystemController</code> regarding its operation state. 
Each
 * subsystem manages its operation state according to its own needs.
 *
 * @author <a href=" Jorelid</a>, <a href=" 
Europe</a> * @version $Id$
 * @since January 2000
 */
public interface SystemModuleController
{
        /**
         * Method moving the controlled module from the current
         * operation status to a new one. It is the responsibility
         * of the implementing class of this interface to notify
         * all relevant classes/modules in the application system of the
         * change in operation status.
         *
         * @param newStatus Integer giving the new system operation status.
         *                  Most normal states are given as constants in
         *                  the SystemController interface.
         */  
        public void setModuleStatus(int newStatus);
        /**
         * @return The current module operation status.
         */  
        public int getModuleStatus();
        /** 
         * @return An identification string of this module.
         */
        public String getModuleID();
   /**
    * @return The queueOrder of this SystemModuleController.
    */
   public int getQueueOrder();
   /**
    * Sets the queueOrder of this SystemModuleController. The queueOrder
    * is a suggestion for the order of handling when the system as a whole
    * migrates from a lower operation level to a higher one.
    * Lower queueOrder modules are processed before higher queueOrder ones.
    *
    * The opposite is true when decreasing the system operation status.
    */
   public void setQueueOrder(int newQueueOrder);
        /**
         * @return <code>true</code> if the system is changing operation
         *         states. <code>false</code> if the whole application
         *         module is running within a certain operational state.
         */
        public boolean isStatusChangeInProgress();
        /**
         * Adds a direct dependency (parent) module to this 
SystemModuleController.
         * A parent module is one whose services are used by classes 
managed by
         * this SystemModuleController. Before changing the operational 
status
         * of any of the direct parent modules, the status of this 
SystemModuleController
         * must first be changed.
         *
         * @param aModule A SystemModuleController which this 
SystemModuleController
         *                depends upon.
         */
        public void addParentModule(SystemModuleController aModule);
        /**
         * @return A List of all parents of this SystemModuleController.
         */
        public List getParentModules();
}

All subsystem facade objects are created and added to the setupApplicationSubsystems method of the TestClass class. Note that TestClass implements the ModuleManager interface — the this variable refers to the ModuleManager.

Listing 2. setupApplicationSubsystems method

  /**
   * Method that sets up the subsystems of this application.
   */
  protected void setupApplicationSubsystems()
  {
     // Create two SystemModuleControllers
     SystemModuleController s1 = new 
TestSystemModuleController("BaseModule", 20);
     SystemModuleController s2 = new 
TestSystemModuleController("HighLevelModule", 50);
     // Setup subsystem dependency
     s2.addParentModule(s1);
     // Register subsystems with the 
ModuleManager
     this.addSystemModule(s1);
     this.addSystemModule(s2);
     // Log done.
     System.out.println("[TestClass setupApplicationSystem]: All 
modules registered " +
                        "with automagic module management.");
  }

Invoking the launch method

Your system has indeed created an object from a class specified in the configuration file, thus fully decoupling the application initialization class from the application setup code. This process was covered in Part 1, so the only remaining issue is to call a method in that class to actually launch the system. A setup system can do all the initialization and required object creation from within a single constructor call; the system can then work properly. However, some systems will be better off not launching vast numbers of new threads from within the constructor of one of its classes. This is especially true of systems where race conditions may occur for reasons beyond your control. Such systems need to construct and set up all required objects, then have a specific method called afterward to launch the normal working thread.

To keep with the tradition of creating the launcher object completely from configuration-file specifications, you should place the name of the method to invoke in the configuration file. Since all proper object setup should have been done from within the launcher class’s constructor, the launcher method should rarely, if ever, need any parameters to control its execution. The Invocation pattern of the launcher method can therefore be greatly simplified.

You will have to provide the launcher method name in the configuration file. The only modification of this file is the single, boldface row at the end of the following list, which provides the startupMethod directive.

Listing 3. Augmented configuration file

############################################
#                                          #
# jGuru Sample Configuration file          #
# Created by Lennart Jörelid, [email protected] #
#                                          #
############################################
startupClass=com.jguru.initHandler.TestClass
arg0=A custom config string
type0=java.lang.String
arg1=42
type1=java.lang.Integer
startupMethod=loadSystem

These equivalent Java statements should be performed after parsing the configuration file above:

Listing 4. Pseudo code from configuration file

TestClass launcher = new 
com.jguru.initHandler.TestClass("A custom config string", 42);
launcher.loadSystem();

The first statement sets up all the objects in the system, as was demonstrated in Part 1. Your task is to invoke the loadSystem method from instructions read in the configuration file. This is another task handled well by the Java reflection API of package java.lang.reflect. Thus, the Startup class is slightly altered to handle the method invocation. The alterations from the former Startup class are small, and have been marked in bold below:

Listing 5. Startup.java

// Copyright (c) 1999 jguru.com
// 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 <a href=" 
J&ouml;relid</a>,
 *                  <a href=" 
Europe</a>
 * @version $Id$
 * @since January 2000
 */
public class Startup
{
    /** The ObjectFactory used to create the dynamic 
(first) 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;
    /** All parsed directives found in the config 
file. */
    private Map              configDirectives;
    /**
     * 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 variables
        this.confParser = new PropertyConfigParser();
        this.objFactory = new SimpleObjectFactory();
        this.configFilePath = configFilePath;
    }
    /**
     * Main setup method, that reads the configuration file
     * interprets all directives, and creates the launcher
     * dynamic object that could startup the entire application.
     */
    public Object doSetup() throws StartupException
    {
       // Read all directives from the config 
file
       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 of the 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 void invokeStartupMethod(Object target) throws 
StartupException
    {       // If the user has defined a startup 
method, invoke it.   String startupMethodName = (String) 
configDirectives.get("startupMethod");
       if(startupMethodName == null || 
startupMethodName.equals(""))
       {
          System.out.println("Found no startup method in config 
file.");
          System.out.println("Exiting startup.");
          return;
       }
       System.out.println("nInvoking method " + 
startupMethodName);
       Method[] allMethods = target.getClass().getDeclaredMethods();
       for(int i = 0; i < allMethods.length; i++)
       {          // Make sure the method have no 
parameters.      if(allMethods[i].getName().equals(startupMethodName)
             && allMethods[i].getParameterTypes().length == 0)
          {
                // This is the method to 
invoke.            try
                {
                   // Finally invoke the 
method.               allMethods[i].invoke(target, null);
                }
                catch(InvocationTargetException itex)
                {
                    System.err.println("Could not invoke startup 
method " + startupMethodName);
                    System.err.println("" + 
itex.getTargetException());
                }
                catch(Exception ex)
                {
                    System.err.println("Could not invoke startup 
method " + startupMethodName);
                    System.err.println("" + ex.getMessage());
                }
          }
       }
    }
    public static void main(String[] args)
    {
       // First, create a dynamic setup object from 
the
       // given directives of the configuration file.
       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);
       // Invoke the startup method on the 
dynamically created object.
       launcher.invokeStartupMethod(dynObject);
    }
}

What happens in the startup method?

It is important that you place the startup method in the same class as the configuration launcher constructor. This imposes no further limitations on the system, since you may easily implement another setup and launcher class, or subclass the existing one. After changing the string entries in the configuration file, the setup and launching processes are now completely controlled by the new Startup launcher.

The only task you’re required to perform in the startup method is setting the system state into normal mode. In the example loadSystem method, the application system is brought to normal mode and then immediately shut down again.

Listing 6. loadSystem() method

 /**
   * Sample launcher method that changes the operation level of the system.
   * This would be replaced with a 'go to normal operating state' 
instruction,
   * as system halts would probably be initiated by passing the JVM an 
operating
   * system signal or receiving a message on a server socket.
   */
  public void loadSystem()
  {
       // Log.
       System.out.println("[TestClass loadSystem]: Moving system into 
normal operation status level.");
       // Goto NORMAL operation state
       this.setSystemStatus(this.NORMAL);
       // All is well.
       System.out.println("[TestClass loadSystem]: System operating 
within normal parameters.");
       // Log.
       System.out.println("[TestClass loadSystem]: Bringing system to 
halted state.");
       // Goto HALT operation state
       this.setSystemStatus(this.HALT);
       // All is done.
       System.out.println("[TestClass loadSystem]: System 
halted.");
  }

When running the system using the two Test classes, the log below appears. Note that module dependencies are preserved during operation state changes; the BaseModule is always kept running at a certain operation state before starting the HighLevelModule on that same operation state. Conversely, the HighLevelModule is always shut down on a particular operation state prior to shutting down the BaseModule from that operation state.

Listing 7. System log

TestClass setupApplicationSystem]: All modules registered 
with automagic module management.
Created instance of com.jguru.initHandler.TestClass
[Instance]: TestClass(String, int) called with parameters: (A custom 
config string, 42)
Invoking method loadSystem
[TestClass loadSystem]: Moving system into normal operation status level.
 --> Module 'BaseModule' changed status to 1
 --> Module 'HighLevelModule' changed status to 1
 --> Module 'BaseModule' changed status to 2
 --> Module 'HighLevelModule' changed status to 2
[TestClass loadSystem]: System operating within normal parameters.
[TestClass loadSystem]: Bringing system to halted state.
 --> Module 'HighLevelModule' changed status to 1
 --> Module 'BaseModule' changed status to 1
 --> Module 'HighLevelModule' changed status to 0
 --> Module 'BaseModule' changed status to 0
[TestClass loadSystem]: System halted.

Tying the knot

The full JavaDoc reference and source code for the setup system are available for download in Resources. Download it, then follow the execution path through the application framework and play around with your own implementations of the various interfaces.

Conclusion

When developing a setup subsystem, strive for decoupling the application system root class and the implementation of the setup subsystem. The less tightly coupled the two are, the smaller the system maintenance cost. The reflection classes of the Java API greatly facilitate the introspection required to implement that decoupling. Use a reflective approach when designing your setup subsystem, and your overall maintenance cost of the subsystem will be dramatically reduced.

A setup subsystem must be decoupled from the application subsystems it controls. Using a unified subsystem facade API, each subsystem may be controlled in a simple fashion. Run-level systems, such as the simple one described in this article, enhance control over large systems by defining a set of fixed system states in which all subsystems should be stable.

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 currently writing a book on
server-side Java patterns. Lennart is a fan of skiing, acting, and
sci-fi. 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 jointly producing articles and
free educational Web events.

Source: www.infoworld.com