Use JNDI to share objects between different virtual machines

Share remote objects between different virtual machines without the need for an object request broker

Before I turn to this month's tool, I would like to address feedback from my previous article. I'll respond to reader comments on better design, performance problems, and bugs.

Better design

One reader commented that the Cachetable I presented in last month's column violates the Liskov Substitution Principle (see Resources). This principle basically states that if B is a subclass of A, and C is a subclass of A, then B should be able to subclass C without any problems, and vice versa. In other words, any class that currently extends Hashtable should be able to also extend Cachetable without any side-effects. It wasn't my original intention to support this notion, but the reader is correct in asserting that it is good object-oriented design.

Performance problems

A few programming veterans immediately noticed that the ping() method has a glaring flaw: it's synchronized! This means that the cache is locked during house cleaning, which leads to performance issues if you have a rather large collection. This issue can be resolved by incorporating reader/writer locks, but that topic is out of the scope of this series. If you're interested, you can read more about reader/writer locks in Part 7 of Java Toolbox columnist Allen Holub's series on thread issues.

Bugs

One reader pointed out that the get() method has a potential for NullPointerExceptions. The return value from super.get() isn't checked, and could therefore be null if a bad key is passed in. We can avoid this with a simple change:

   public synchronized Object get( Object key ) {
     Object o = super.get( key );
     if( o != null )
     {
       return( ( (TimeWrapper) o ).getObject() );
     }
     else
     {
       return null;
     }
   }

Introduction to this month's 'cool tool'

This month's tool allows you to share objects between different virtual machines. It also allows you to store an object, have a process die, and then be able to retrieve that object once the process restarts. All of this is achievable without the complexity of database mapping or remote object architectures (like CORBA and EJB). The key concepts for this tool are serialization, persistence, and JNDI. Serialization allows you to "package" an object instance for storage and transfer. Persistence is the storage part that requires the serialization. And JNDI is Sun's standard API for interfacing with directory and naming servers. I will discuss each concept briefly, then dive into the tool itself.

Serialization and persistence

Serialization is the technique by which an object can store and restore its state, usually to and from a stream of bytes. When you serialize an object, you're basically breaking it down into its most primitive values (integers, booleans, and strings). These primitive values must be managed in a predetermined format and order so that you can deserialize the object, thus restoring the object to its original state.

Support for serialization allows an object to be persistent. In the simplest terms, this means that an object can survive from one program instance to another. For example, let's say you start up a Java program with some sort of widget. You modify the widget in some manner -- changing its color, for example. You stop the application. Then, the next time you run the application, the widget is magically the same color as when you last stopped the application. This is accomplished by simply serializing the widget to a file when the application stops, and deserializing the widget from the same file when the application starts. The file will be very small, because it only contains the integer value representing the color of the widget.

Being the wonderful language that it is, Java handles serialization for you (in JDK 1.1 and later versions). All you have to do is make your object implement the java.io.Serializable interface, and it's automatically serializable; there are no methods to be implemented. Actually, serialization isn't quite that simple. There are a few other little details that you should be aware of, such as dealing with nested containers and transient data (information that isn't vital to the object's state, and can be discarded). But these topics are outside of the scope of this article.

(Note: If you happen to be unlucky enough to still be working with the 1.02 version of the JDK, head over to ACME Laboratories for a great set of serialization utilities (see Resources for a link to this site). Not only do they work with the old JDK 1.02, but they're free!)

Now that you understand the basics of packing up and storing an object, we can talk about transferring that object to a remote server and then finding it again when needed. This is where JNDI comes in.

JNDI and directory services

The Java Naming and Directory Interface (JNDI) is a high-level API that allows you, the programmer, to work with many different naming and directory services through a single consistent interface. A naming or directory service, as its name implies, is a service that allows you to store and query names-relative information. This data can be the names of computers (as in the DNS), the login information of users (as in the NIS), or the names and addresses of people (which can be stored in X.500 or LDAP).

Not only does JNDI abstract the interfaces to the diverse naming systems, but it goes one step further and allows you to store and retrieve actual objects. The only catch is that the objects must be serializable. When an object is placed into or retrieved from a naming service, it is serialized into a stream of bytes for transport over the network and for storage.

The JNDIHashtable

This month's tool, the JNDIHashtable, uses the power of serialization, persistence, and JNDI to store objects on a remote server, rather than in local memory. The tool extends a standard Hashtable and should be used like one (though, as the reader feedback above points out, it doesn't conform to the Liskov Substitution Principle). When you put an object into the JNDIHashtable, it is actually serialized and transported to the naming server. When you ge an object out, the tool looks for the object on the remote server, retrieves it, and returns it to the process. I originally wrote this tool to help overcome a problem with stateful-session Enterprise JavaBeans.

Session beans are supposed to be short-lived single-client objects. The client connects to the bean (remote object), calls a couple methods, then disconnects and everything is done. Stateful-session beans are a hybrid type; they allow the client to disconnect from the object before finishing all of its work, and then reconnect at a later time.

The problem with session beans is that you need to keep track of a handle in order to reconnect to them. A handle is like a reference to an object in local memory, and in fact that's what it is, except that the object in local memory knows how to find and connect to a remote object.In my case, I was accessing the beans from multiple Web servers. Each time a Web page was accessed, a server-side script would run. The script needed to use a particular bean each time it ran. Due to a load-balancing mechanism on the front end of the Web servers, there was no guarantee that a user's browser would connect to the same Web server for subsequent requests. This meant that I couldn't store any state information, like the bean's handle, in the Web server's process space. By using the JNDIHashtable, I could store the bean's handle (and any other data) in the naming server (remotely) and access it from whichever Web server executed the next script.

However, in order to do this cleanly, we must add a couple restrictions to the way objects are put into the tool:

  • The key must be a String
  • the value object must be serializable

Why does JNDIHashtable have these restrictions? Because the object that you put into the JNDIHashtable isn't stored in the local memory of the running virtual machine; it is serialized and stored in a naming service. The naming service must be accessible via JNDI. This is how other applications running in separate virtual machines can access the same objects. All of the applications that are using the JNDIHashtable can share objects with each other.

The code

Coding the JNDIHashtable is simply a matter of extending Hashtable and overriding all of the necessary methods to ensure that objects are stored on the remote server rather than in local memory. In this article, I'm only going to cover the basics: the put(), get(), and remove() methods, and keys (get a list of all object mappings). The complete code can be found here: JNDIHashtable.java.

We start out with the constructors. These are simply a redeclaration of the standard Hashtable constructors, except for one minor difference: the connection to the remote server. You'll see that the first constructor calls the second, and the second calls the third. The third constructor performs the connection by calling the connect() method. The connect() method connects to the naming service via JNDI. The Context object is the handle to the naming service; more on that in a moment.

import java.util.*;
import javax.naming.*;
public class JNDIHashtable extends Hashtable
{
  private Context context;
  public JNDIHashtable() {
    this( 101 );
  }
  public JNDIHashtable( int initialCapacity ) {
    this( initialCapacity, (float) 0.75 );
  }
  public JNDIHashtable( int initialCapacity, float loadFactor ) {
    super( initialCapacity, loadFactor );
    connect();
  }

In order to connect to a naming service through JNDI, you have to create and use a Context object. The Context object takes a Hashtable for its constructor. You must populate the Hashtable with the necessary configuration data. This will differ from one service to the next, and depend on the driver provided. At the very least, you will have to provide a factory class name (which creates the connections) and a URL (which specifies where the service is running). In the connect() method below, you'll see that I populate the Hashtable with settings specific to the BEA Weblogic (formerly known as Tengah) server. You don't need to worry about these details here; setting up a naming service is outside the scope of this article.

  private void connect() {
    Hashtable props = new Hashtable( 3 );
    props.put( Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.TengahInitialContextFactory" );
    props.put( Context.PROVIDER_URL, "t3://localhost:7001" );
    props.put( "com.ejbhome.naming.spi.rmi.hostname", "t3://localhost:7001" );
    try {
      context = new InitialContext( props );
      context = context.createSubcontext( "TED" );
    }
    catch( NamingException e ) {
      e.printStackTrace();
    }
  }

Notice that I used the initial context to create a subcontext. The initial context is the top-most namespace -- where you have the objects mapped to their String names. Subcontexts stem from the initial context like a tree. By using a subcontext, we reduce the chances of naming collisions with other applications that might be using the same naming service -- two different clients trying to store two different pieces of data under the same name. In this case, I have used my initials to label the subcontext. If you wanted to use JNDIHashtables for different storage spaces within the same application, and you were concerned about naming collisions, you could make the subcontext configurable (perhaps via another constructor).

Now we need to override some of the basic Hashtable methods. As I mentioned above, there are some restrictions on what you can put into a JNDIHashtable. The key must be a String and the value must be serializable. Thus, we override the put() method to check for these two conditions. If the conditions are not met, an exception is thrown. If the conditions are met, the object is serialized and sent to the naming service. Here's the code:

  public synchronized Object put( Object key, Object value ) {
    // key must be a string!
    if( ! ( key instanceof String ) ) {
      throw( new JNDIHashtableException( "The key must be a String!" ) );
    }
    // value must be serializable!
    if( ! ( value instanceof java.io.Serializable ) ) {
      throw( new JNDIHashtableException( "The value must be serializable!" ) );
    }
    Object previous = get( key );
    try {
      try {
        context.bind( (String) key, value );
      }
      catch( NameAlreadyBoundException ex ) {
        context.rebind( (String) key, value );
      }
    }
    catch( NamingException e ) {
      e.printStackTrace();
    }
    return( previous );
  }

The JNDIHashtableException is a simple subclass of Exception.

class JNDIHashtableException extends NullPointerException
{
  JNDIHashtableException( String msg ) {
    super( msg );
  }
}

The get() method is very similar. We check the key to ensure that it's a String, and then attempt to retrieve the object from the naming server:

  public synchronized Object get( Object key ) {
    // key must be a string!
    if( ! ( key instanceof String ) ) {
      throw( new JNDIHashtableException( "The key must be a String!" ) );
    }
    try {
      return( context.lookup( (String) key ) );
    }
    catch( NameNotFoundException e ) {
      // this is fine!
    }
    catch( NamingException e ) {
      e.printStackTrace();
    }
    return( null );
  }
1 2 Page 1
Page 1 of 2