woensdag 5 november 2008

Making an RMI service pluggable

This post is about making web services (such as RMI) flexible such that their functionality can be extended through plugins.

Remote Method Invocation (RMI) is a mechanism that allows you to access remote functionality over a network. In that sense, it is just a remote procedure call, for which multiple frameworks exist, both java-specific and non-java-specific. RMI, however, appears to be among the top performers if it comes to speed.

The ServiceLoader API allows you to develop extensible systems, that is, systems whose functionality can be extended without access to the original source code. This is usually done by defining an interface for the extensions. This interface, called the extension point, defines on a high level what the extension should do, e.g., it should process an XML file and yield another XML-file, it should add an entry to a menu, etc. The interface does not specify what goes on within an implementation of an extension. When the host application starts, it looks for registered plugins and does something with these. Besides the ServiceLoader API, other frameworks for developing extensible applications exist. (I am only slightly familiar with those, and i would like to see a good review of those.)

Extensibility is often confused with modularity, but this is not correct: A modular application need not be extensible, but an extensible application *is* necessarily modular.

Another source of confusion (at least to me) is formed by the multiple terms used to designate plugins: These are also called extensions, modules, add-ons and sometimes services. The ServiceLoader API calls them services, hence the name ServiceLoader.

If you are designing a client-server system for remote procedure calls in Java, and you want the server to have pluggable functionality, then you can use service mechanism, such as RMI, together with a plugin mechanism, such as the ServiceLoader API. In this post i will show a toy example of such an architecture. There are a number of steps you should take to get this going.

1. Define the plugin interface.

The first thing we must do is define the interface that the plugins must implement. In this case, the plugins are RMI services. Each service must have methods for starting and stopping it, and it must have a method yielding its name. Thus we have
package pluginRMI.spi;

import java.rmi.Remote;

public interface IPluginRMIService {
public void start();
public void stop();
public String getName();
}

As you see, the inteface is placed in package pluginRMI.spi. Spi stands for `service provider interface'. This naming convention was adopted from SUN's tutorial and it comes from the fact that ServiceLoader calls plugins services. Since RMI also works by implementing an interface, it is important not to confuse this interface with the interfaces that the individual RMI services will implement. This will become clear below.


2. Create some plugins.


The individual plugins are RMI services in our case. Hence, to create the plugins we must first create an interface for each RMI service that we want to start as a plugin. These interfaces are places in package `common' as they are shared between the RMI server and the RMI client.

The first interface defines a simple calculator service that can add and multiply integers:
package pluginRMI.common;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface ICalculator extends Remote {
public Integer add(Integer a, Integer b) throws RemoteException;
public Integer multiply(Integer a, Integer b) throws RemoteException;
}

The second interface generates a String containing a fortune:
package pluginRMI.common;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IFortuneTeller extends Remote {
public String getFortune() throws RemoteException;
}


Next, we have to implement the actual plugins. I only show the implementation of the FortuneTeller plugin. Note that this class implements two interfaces: one for the ServiceLoader and one for the RMI service.

package pluginRMI.implementations;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Random;

import pluginRMI.common.IFortuneTeller;
import pluginRMI.spi.IPluginRMIService;

public class FortuneTellerRMIService implements IPluginRMIService,
IFortuneTeller {

public FortuneTellerRMIService() {
}

public String getName() {
return "FortuneTeller";
}

public void start() {

// Most of this code can probably be moved outside of this class...
try {
IFortuneTeller stub = (IFortuneTeller) UnicastRemoteObject
.exportObject(this, 0);

// Bind the remote object's stub in the registry
Registry registry = LocateRegistry.getRegistry();
registry.rebind(getName(), stub);

System.err.println("RMIServer " + getName() + " ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();

}
}

public void stop() {
try {
Registry registry = LocateRegistry.getRegistry();
registry.unbind(getName());

System.err.println("Stopped RMIServer " + getName());
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}

public String getFortune() throws RemoteException {
Random random = new Random();
if (random.nextBoolean()) {
return ("Live in a world of your own, but always welcome visitors.");
} else {
return ("The hardest part of climbing the ladder of success is getting throug "
+ " the crowd at the bottom.");
}
}
}


(Aside: When the IPluginRMIService is changed into an abstract class it may be possible to move the code in start() and stop() into the abstract class, preventing duplication among plugins.)


3. Create the host application.


The host application is responsible for locating and starting the RMI services that have been registered with the system. This is the place where the ServiceLoader API comes into play. The code:

package pluginRMI;

import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;

import pluginRMI.spi.IPluginRMIService;

public class PluginRMIServiceLoader {

public static void main(String[] args) {

ServiceLoader<IPluginRMIService> loader;
loader = ServiceLoader.load(IPluginRMIService.class);

try {
System.out.println("Starting all RMI plugins available...");
Iterator<IPluginRMIService> pluginRmiServices = loader.iterator();
while (pluginRmiServices.hasNext()) {
IPluginRMIService plugin = pluginRmiServices.next();
System.out.println("Starting " + plugin.getName());
plugin.start();
}

} catch (ServiceConfigurationError serviceError) {
serviceError.printStackTrace();
}
}
}




4. Register the implemented plugins with the ServiceLoader


In order for the ServiceLoader to be able to locate the implemented plugins, they must be registered. This is done by entering some meta data in META-INF/services. Here you must create a text file of which the name equals the full class name (incl. package) of the plugin interface. In our case this filename is pluginRMI.spi.IPluginRMIService. The contents of that file is simly the list of class names implementing the plugin interface.

pluginRMI.implementations.CalculatorRMIService
pluginRMI.implementations.FortuneTellerRMIService


Obviously, the ServiceLoader can use this information to locate the plugins.


5. Create an RMI client.


The final step is to create the client for the RMI service. This is straightforward.
package pluginRMI.client;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import pluginRMI.common.ICalculator;
import pluginRMI.common.IFortuneTeller;

public class RMIClient {

private RMIClient() {
}

public static void main(String[] args) {

String host = (args.length < 1) ? null : args[0];
try {
Registry registry = LocateRegistry.getRegistry(host);

// First do some stuff with the calculator service...
ICalculator stubCalc = (ICalculator) registry.lookup("Calculator");
Integer sum = stubCalc.add(1, 1);
Integer prod = stubCalc.multiply(2, 2);
System.out.println("Calculator service generated values " + sum
+ " and " + prod);

// Then do some stuff with the fortuneteller
IFortuneTeller stubFort = (IFortuneTeller) registry
.lookup("FortuneTeller");
System.out.println("FortuneTeller service generated fortune: "
+ stubFort.getFortune());
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}



6. Start the application.

  1. Start the RMI registry.
  2. Start the host application. Don't forget to set the correct value for the codebase VM argument or the RMI services will not start. (In eclipse this is -Djava.rmi.server.codebase=file:${workspace_loc}/PluginRMI/ under run configurations | arguments tab | VM arguments .)
  3. Start the RMI client.
Et voila, we have a system into which arbitrary functionality can be added at the server side. Of course this only makes sense when the plugins can share data among them and with the host application, but this is easy to achieve by e.g. giving them a pointer to a common Map.

I hope this post was helpful to you in designing a flexible architecture. Sorry for the crappy layout here and there, but i just can't work with wysiwyg editors.