Java Tip 56: How to eliminate debugging problems for RMI-based applications

Make your development of RMI-based applications much easier -- use an in-process server to develop and test your client/server code

Developers with experience using RMI for product development largely agree that debugging client/server code presents many difficulties. Stepping into the server from the client or vice versa is not fun for many reasons. Debugging client/server programs using the RMI API is particularly painful, because the execution path jumps back and forth between client and server.

This article presents a simple and elegant solution to the many problems associated with RMI-based product development. The article assumes you have a reasonably good understanding of RMI-based client/server development.

Problems in debugging

Running two debuggers on one machine requires us to have reasonably high-end machines. And switching between two applications is always a nuisance. If we run the debuggers on two different machines, then we need to use two keyboards to step through the debugging process. These petty debugging logistics distract the developer from problem solving.

A common source of trouble is running RMI-based applications on a computer or laptop that is not configured adequately for TCP/IP. This is a big problem for developers who prefer to use their laptops or home PC for product development. Most people use modems to connect to the Internet -- and their TCP/IP is configured to obtain a dynamic address each time they connect to their ISP. So, whenever an RMI application is run, the dial-up dialog box comes up to put the computer on a valid network. To avoid this problem, we can tweak the TCP/IP and COM port settings so that we can run RMI applications without connecting to the ISP -- but then we cannot connect to the Internet while we are developing RMI applications. Reconfiguring the computer (or laptop) frequently is not a feasible solution. As a whole, working from home is not an easy option for those using RMI.

This article describes a simple design technique that allows us to debug the server and client in a single process, without breaking any RMI guidelines or the existing code base. It describes how RMI can be bypassed entirely when you want to debug the client and server code in any debugger in a traditional style (within a single process). The same codebase can be used to run the client and server in separate processes or in a single process, merely by toggling a boolean flag in the client.

Benefits from this design technique

You can implement this technique for your existing codebase in half a day or in a full day, depending on the number of remote interfaces your server supports -- and depending on your typing speed. Once everything is in place, developers can reap the following benefits:

  1. Code can be written and tested on a single machine, in a single process.
  2. There is no need to have a network card and a valid TCP/IP address.
  3. Any JDK 1.1-compatible debugger can be used for debugging both client and server.
  4. Development can be done at home without disturbing the ISP settings. There is no need to get online to obtain a dynamic IP address.
  5. RMI-based products can be demonstrated on a laptop without changing any network or TCP/IP settings.
  6. While the server application is running, a modem can be used to get on the Internet at will.
  7. There is no need to run an RMI registry at the time of debugging.

This design technique requires us to implement the server to support two modes of execution. These modes are:

  1. Remote (standalone) server mode
  2. In-process server mode

Remote (standalone) server mode

In this mode, the server process will be kicked off on a server machine using a simple startup program. This startup program creates a server object and binds it to the registry so that clients can do a lookup through Naming.lookup(). If the server object is creating one or more server-side proxy objects that support remote interfaces, then the server takes the responsibility of registering them with the local RMI registry. Any RMI-based server supports this mode by default.

In-process server mode

In the second mode, the server assumes that it is running inside the client's process. An in-process server is functionally a subset of the remote server. In this mode, the server assumes that the client has direct access to all remote interfaces supported by the server. This is possible because the client and server are sharing the same process space.

In this mode, server objects (server proxies) that implement remote interfaces need not be registered with the local RMI registry. Also, the server avoids invoking the Naming.bind() and Naming.rebind() methods. In addition, it avoids making calls to the API defined in java.net.* classes because the network API methods are not required for setting up communication between client and server. However, the server does not care how the client gets a direct reference to its remote interfaces. The server simply makes sure that all of its services are always available only through remote interfaces. This is precisely the design philosophy of the RMI protocol -- so an in-process server does not break any RMI guidelines.

How does a client communicate with the in-process server?

The client implementation has to be split into two pieces to be able to deploy the design technique described above. The first part of the client code will isolate the RMI-based code from the client implementation. It obtains the remote interfaces on the server using either RMI or through an in-process server. The second part of the client code accepts these remote interfaces to invoke the remote API on the server.

Step-by-step implementation of client/server

Here is a summary of changes required to the client and server code:

  • The server should be modified to support two modes of execution (if required)
  • The client should be split into two parts:
    • The first part acquires a remote server interface
    • The second part accepts the server interface and invokes the client application

The following sections describe these steps in more detail using a sample application.

If your server satisfies the following conditions, then the server can be run as an in-process server without modifying a single line of server code, and you can skip ahead to Step 2. Otherwise, continue with Step 1 below.

Condition 1: Server does not create any remote objects that implement remote interfaces

Condition 2: Server does not use API defined in java.net.* classes

Step 1: Modifications to the server code

The Server object extends from UnicastRemoteObject and implements ServerInterface. Add a constructor to Server that takes a boolean flag to indicate the mode in which this server has to execute. If the in-process boolean flag is set to "true," then this server simply ignores all activity related to RMI. It will not register any proxies and will ask all its aggregates to follow suit. By default this flag is initialized to "false." The following code shows a simple constructor required on the Server object to support the in-process server mode.

    //Non-Default constructor to handle in-process server mode.
    public Server(String    sWorkingDir, boolean bRunAsInProcessServer)
    {
        m_sWorkingDir = sWorkingDir;
        bRunAsInProcessServer = bRunAsInProcessServer;
    }

Step 1-A: Handle server-side proxies

If your server registers one or more remote objects to be used by clients, skip all RMI activity if the in-process flag is set to "true." Insert an if() block around the code that does Naming.bind (), Naming.rebind (), and Naming.unbind () to skip registering and unregistering proxy objects. As shown below, all RMI calls in the server code should be wrapped in a simple if{} block using the m_bRunAsInProcessServer boolean flag.

    if(m_bRunAsInProcessServer == false)
    {
        Naming.rebind(proxyObj, proxyName);
    }

Add a method to the Server class to extract direct references to proxy objects that implement remote interfaces. The client will use this method to access proxy objects, and it will cast these object references to the remote references they support. The client always goes through remote interfaces to access services on the server. It will never access any methods that are not part of remote interfaces. The following method allows the client-side code to directly access the server-side remote objects (proxies) if the client needs to access the API supported by those remote objects. Client code will use this method when it wants to run the server in its own process.

   public  Object[]    getServerSideProxies()
   {
    return m_oServerSideProxies;
   }

Step 2: Modifications to the Client class

The client application has to be split into two classes. The sample code contains two files in the client package. They are:

  • runClient.java -- This class is responsible for obtaining the remote interfaces on the server and starting a client application.
  • Client.java -- This class implements all necessary client logic. It accepts the remote interfaces through its constructor and goes ahead to invoke services on the server.

Step 3: Implement the runClient class

Write a few lines of startup code in the runClient class, which decides the method of connection with the server. We can switch between two modes of the server using a boolean flag. Let's call this flag bUseRMI. If the flag is true, then the startup code tries to connect to the server through the Naming.lookup() method to obtain the remote interfaces on the server. When the flag is false, it creates a Server object and casts the server to obtain the remote interfaces.

    public  static void main(String[] args)
    {
    if((args == null)||(args.length != 2))
    {
        System.out.println("Usage: java com.company.product.client.runClient <usermi> <host_name>");
        System.out.println("Usage: java com.company.product.client.runClient <normi> <working_directory>");
        return;
    }
        boolean bUseRMI = false;
        String  sHostName = null;
        String  sWorkingDir = null;
        
        String  str = args[0];
        
        if(str.compareTo("use_rmi")==0)
    {
        bUseRMI = true;
                sHostName = args[1];
    }
    else if(str.compareTo("no_rmi")==0)
    {
        bUseRMI = false;
            sWorkingDir = args[1];
    }
    else
    {
        System.out.println("Usage: java com.company.product.client.runClient <use_rmi> <host_name>");
        System.out.println("Usage: java com.company.product.client.runClient <no_rmi> <working_directory>");
        return;
    }
        ServerInterface IServer = null;
      //Obtain Server Interface based on the boolean flag.
        if(bUseRMI)
        {
            
            IServer = getServerInterfacesThroughRMI(sHostName);
        }
        else
        {
            IServer = getServerInterfacesThroughInProcessServer(sWorkingDir);
        }
        if(IServer == null)
        {
            System.out.println("Failed to get Server Interface.");
            return;
        }
      //Start the client and pass in remote server interface.
        Client  client = new Client(IServer);
      //Start client app.
        client.startClientApplication();
    }

As you can see, the following methods take care of isolating the RMI API from the Client class. These methods return a reference to ServerInterface:

    1. getServerInterfacesThroughRMI();
    2. getServerInterfacesThroughInProcessServer();
getServerInterfacesThroughRMI():

This method does a Naming.lookup() to get a reference to a remote server object, then casts the remote object reference to the interfaces it supports. If the server is registering additional remote objects for each client, then this method logs on to the server to find the registry information about those remote objects. Do a few more Naming.lookup() calls to retrieve interface references on all those remote objects. The following code snippet shows how to obtain a remote interface through standard RMI API.

    private static ServerInterface
    getServerInterfacesThroughRMI(String sHostName)
    {
        //I assume that we know the name of the remote object.
        String  sRemoteObjectName = "//" + sHostName + "/" + CoreC.SERVER_NAME;
        Object  oServer = null;;
        try
        {
            //Connect to the remote object.
            System.out.println("starting lookup for " + sRemoteObjectName);
            oServer = Naming.lookup(sRemoteObjectName);
            System.out.println("Connected with " + sRemoteObjectName);
        }
        catch (Exception e)
        {
            System.out.println("runClient: an exception occurred: ");
            e.printStackTrace();
            return null;
        }
        //Fetch server interface on this remote object.
        ServerInterface  intf = (ServerInterface)oServer;
        return intf;
    }

A note about getServerInterfacesThroughInProcessServer()

This method internally creates an instance of the server and casts the server object to the remote interface it supports. (If the server is registering any additional remote objects, then this method creates those objects as well by calling the API defined in InProcessServerInterface. Additional interface references can be obtained by casting the remote objects to appropriate remote interfaces.)

1 2 Page 1
Page 1 of 2