In this section, we shall develop a simple but elegant distributed application using Java RMI technology. In this application [Figure 14.4:], the server program creates and exports a simple remote object (we call it calculator) which provides one method. The method takes two integers as arguments and returns their sum. A client program gets a reference to the remote object and invokes this method on the remote object to calculate the sum of two integers.
Note that we rarely write such a simple RMI application in practice. An electronic calculator will be much useful in solving this problem. However, the application scenario is kept simple so that we can devote more time to understanding the procedures of developing an RMI application than concentrating on the actual problem.
1. Writing an interface
At first, communicating programs must agree upon a protocol. This protocol is expressed in an interface and is usually developed by the service provider. An interface is an abstract description of the methods that may be invoked on an object supporting that interface. To support an interface, object’s class must provide implementations of those methods.
An interface only provides abstract description (signature) of methods without any implementation. The signature provides necessary information such as name of a method, its return type as well as number, order and type of arguments to be passed to invoke the method. This means the signature provides enough information to determine the method call syntax. A program (call it server program/ service provider) creates an object whose class implements this interface (i.e. provides implementations of these methods) and publishes this interface. By publishing an interface, server program conveys the following:
“Hey all of you, I have created an object that is capable of providing a set of important services. If you are interested, you can get these services by calling the corresponding methods on this object. The necessary information (name of the method, argument list and return type) to invoke these methods is given in the interface”.
Since method invocations on remote objects occur in a very different way from local method invocation, an interface for the remote object must be declared as follows:
- The remote interface must be public.
- The remote interface extends (either directly or indirectly) the interface rmi.Remote. The interface Remote is a marker interface and has no methods.
public interface java.rmi.Remote {
}
A remote interface implements this Remote interface only to indicate that its methods may be invoked remotely.
- Each method in the interface must declare that it throws rmi.RemoteException. Note that remote objects may fail in a very different way from local objects. Therefore, every method exposes the additional exception RemoteException so that programmers can handle this failure appropriately.
In our application, remote object wants to provide a single method as follows:
- A method that takes two integers as arguments, adds them up and returns the result
An interface that specifies the above description may then be written as follows:
//Calculator.java
import java.rmi.*;
public interface Calculator extends Remote {
public int add(int a, int b) throws RemoteException;
}
The Calculator interface defines the client’s view of the remote object. The interested programs (call them client programs/service users) make use of this interface to know the details of services (methods) that the object provides. In this way, the Calculator interface helps the clients and server to agree upon a protocol.
This interface extends Remote interface (provided in java.rmi package) and hence becomes a remote interface. Any object, whose class implements this interface, is a remote object whose methods can be invoked from a different JVM.
The interface describes only one method add(). This is the method that remote programs will use to find the sum of two integers. Since, this method will be called remotely, it may fail due to problems related to communication or protocol or server. So, using throws clause, the method indicates that a RemoteException might be thrown by this method during its execution. The exception RemoteException (provided in java.rmi package) is a checked exception. Consequently, a program that calls this method must prepare it to handle this exception explicitly by either catching or re-throwing the exception
A remote interface must be public. Note that a remote interface will most probably be used by classes which do not belong to the same package where the interface is kept. We know that a non-public interface is only accessible from the classes of the same package. Consequently, to make an interface available to every other, it is declared as public. Our Calculator interface is also declared public accordingly.
Note Java’s object serialization procedure is used by RMI to transfer objects. Consequently, objects/ variables that are transferred across different JVMs must be serializable. An object is said to be serializable if its class implements the java.io.Serializable interface. The interface java.io.Serializable does not define anything; it just specifies that the object of the class is serializable. In our application, a client passes two integers to invoke the method add(). Fortunately, int type and all other Java primitive types are serializable. Hence there is nothing to worry about the serialization procedure.
2. Writing Implementation class
An interface merely helps us in determining method call syntax. It does not provide any method implementation (body). The implementation is provided in another class. In general, an implementation class of a remote object is developed as follows:
- It must declare that it implements at least one remote interface.
- It must provide implementations of all the methods specified in the remote interface.
- Optionally define one or more constructor.
2.1. Implementing the remote interface
There are two ways to write an implementation class (say simpleCalculator for our application). In the simplest way, an implementation class extends either java.rmi.server.UnicastRemoteObject or javax.rmi.PortableRemoteObject class [Figure 14.5: (i)]. In this case, the implementation class readily becomes a remote class. The advantage of this scheme is that the objects of this class are exported automatically by the super class’s (UnicastRemoteObject or PortableRemoteObject) constructor when they are created. So, we don’t have to export them explicitly. However, the problem of this scheme is that, the implementation class cannot extend any other class further as Java does not support multiple inheritance for classes. So, it is not a good idea to write implementation classes extending UnicastRemoteObject or PortableRemoteObject.
To avoid the aforesaid problem, there exists another (probably better) scheme. In this scheme implementation class does not extend UnicastRemoteObject or PortableRemoteObject classes which leave a provision of extending other classes if required later [Figure 15.5: (ii)]. However, instances of this implementation classes have to be exported explicitly, Fortunately, both UnicastRemoteObject and Activatabie classes provide several static overloaded versions of exportObject() method for explicit export of remote objects. In this case, though we have to write some extra piece of code, we shall have a provision to extend other classes if really necessary.
In our class implementation, the last described procedure is followed. Accordingly, a sample class is developed whose source code (stored in file SimpleCalculator.java) is given below:
//SimpleCalculator.java
import java.rmi.*;
public class SimpleCalculator implements Calculator {
public int add(int a, int b)
{
System.out.println(”Received: ” + a + “and” + b);
int result = a + b;
System.out.println(”Sent: ” + result);
return result;
}
}
The implementation class in our example is SimpleCalculator. It declares that it implements a remote interface Calculator as follows:
public class SimpleCalculator implements Calculator {
In general, an implementation class may implement any number of remote interfaces. An implementation class may also extend any other implementation class of a remote interface.
2.2. Providing method implementation
The implementation class must provide definition (body) of all the methods specified in the remote interface(s). For our implementation class SimpleCalculator, the only method add() is defined as follows:
public int add(int a, int b)
{
System.out.println(”Received: ” + a + ”and” + b);
int result = a + b;
System.out.println(”Sent: ” + result);
return result;
}
The implementation of add() method is very simple. It returns the sum of two integers passed to this method. The System.out.println() methods are used only to print useful messages so that we can track this method invocation.
Note that the method add() does not have to indicate that it throws any exception because its body itself does not throw RemoteException nor does it throw any other checked exceptions.
When a class declares that it implements an interface, it basically promises to the compiler that it will provide method bodies for all the methods specified in that interface. We know that methods of an interface are implicitly public and abstract. So, if the implementation class doesn’t obey its promise (it it does not provide definition of all methods), it becomes an incomplete class, which by definition is called an abstract class. In this case, the compiler will figure this fact out if the class itself does not declare it as abstract class.
In an implementation class, it is possible to define methods not specified in the remote interface. However, those methods can only be invoked locally (i.e. within the virtual machine running the service) and cannot be invoked remotely.
The type of arguments to, or return values from, remote methods may be any valid type in Java, including objects. For objects, the only requirement is that they implement the interface java. io.Serializabie. It is good to know that almost all classes in java.iang and java.utii packages implement the Serializable interface. However, certain types are inherently non-serializable and cannot be passed to or returned from a remote method. Examples of such type include classes related to threads, file descriptors, socket connection, database connection, that makes sense only within a single address space.
The following rules are employed when objects are passed to a remote method.
When a local object is passed to a remote method as an argument, the method’s formal parameter refers to a local temporary object which is an exact copy of the actual object. Consequently, if a method is called in the remote method through the formal parameter, invocation of the method occurs on local object not on the actual object that was passed as an argument. Any changes to this local object’s state in the remote method are reflected only in the receiver’s local copy, not in the caller’s original object. Similarly, any changes to the original object’s state by the caller are not reflected in the receiver’s copy. Note that, class definition of actual object must exist in the remote method’s JVM.
However, if the object passed to a remote method is itself a remote object, method’s formal parameter refers to a local temporary object which is a proxy to the actual object. In this case, a method call through the formal parameter results in a similar method invocation on local proxy object which in turn forwards the method invocation information towards the actual object. A detailed description of this procedure is given later in this chapter.
2.3. Writing Constructor
Writing the constructor is not mandatory if implementation class does not extend java.rmi.server.UnicastRemoteObject or javax.rmi.PortableRemoteObject class. However, if the implementation class extends any one of these two classes, a constructor is mandatory for the following reason:
When an implementation class extends java.rmi.server.UnicastRemoteObject or javax. rmi.PortableRemoteObject class, objects of the implementation class are exported by the super class’s constructor automatically upon creation. Since, during this export, super class’s constructor could potentially throw a java.rmi.RemoteException, we must define a constructor that throws a RemoteException, even if the constructor does nothing else. Otherwise, the Java compiler produces an error message.
Since our implementation class does not extend any of those two classes, we have not written any constructor. However, a suitable constructor may always be written if initialization of variables of each newly created instance of the class is needed.
3. Writing an RMI Server
This is a simple Java application program (call it CalculatorServer.java) containing the well- known main() method. This program usually performs the following steps:
- Creates an instance of implementation class
- Exports it so that the object can receive method invocation from remote client
- Registers the object to a registry so that remote client can get remote reference to this object
3.1. Creating a remote object
Creating an instance of the implementation class uses the same syntax used for creating ordinary objects. So, the following piece of code creates an instance of our simpieCaicuiator class.
SimpleCalculator calculator = new SimpleCalculator();
3.2. Exporting the object
The object created above is an ordinary object and is not capable of handling method invocation requests that come from a remote program. To add this capability, it has to be exported. Exporting an object basically means making the object capable of receiving invocations of its methods from remote clients. Note that our implementation class does not extend UnicastRemoteObject or PortableRemoteObject class. So, we have to export the object explicitly using static exportObject() method of the UnicastRemoteObject (or PortableRemoteObject) class as follows:
Calculator stub = (Calculator)UnicastRemoteObject.exportObject(calculator, 0);
The first argument of exportObject() method is an object to be exported. The second argument is an int that specifies the TCP port to be used to listen for incoming remote invocation requests for the object. Usually a 0 (zero) is used for this value which indicates that the port has to be chosen at runtime by RMI or the underlying operating system. However, programmers may specify a specific port to be used for listening. If exportObject() method returns successfully, our SimpleCalculator object becomes ready to process incoming remote invocations.
Let us now understand the functionality of exportObject() method in detail. This is necessary for programmers who want to understand the implementation of Java RMI architecture. A clear understanding of this architecture enables programmers to develop sophisticated network applications without any hassle.
The exportObject() method essentially creates and installs a proxy for the calculator object at the server end passing the calculator object to the proxy. This proxy is known as skeleton. This proxy has a reference to the calculator object. The task of the skeleton broadly is as follows:
It creates a TCP ServerSocket object and listens for the incoming connection requests on the TCP port specified by the second argument. Upon establishing a connection, The ServerSocket waits for the incoming remote method call on behalf of our calculator object.
Whenever a request for method call (in the packed format) comes to this skeleton, it unpacks the request to obtain the method name and arguments to be used for this method. This unpacking procedure is also called un-marshalling. Note that the calculator object and its skeleton belong to the same JVM and skeleton has also reference to this local calculator object. So, skeleton can invoke the desired method on the actual object and get the result. It then packs (called marshalling) the result and sends it back to the caller.
The method exportObject() creates and returns another proxy for this object to be uploaded to the object registry. This proxy is called stub. The stub knows the port the skeleton listens on as well as the IP address of the computer. This means the stub knows all the information about the skeleton and can communicate with e skeleton as and when necessary. The object registry essentially provides this stub object on request. The type of this stub must be Calculator, not the SimpleCalculator. This is because stub for a remote object (SimpleCalculator in this case) implements the interface (Calculator) which is implemented by the exported object.
The interesting part of exportObject() method is that it creates and loads class files for stub at runtime (if one not found). Consequently, we don’t have to generate class files for stub using rmic compiler in Java 5.0 and later versions. This method also has another overloaded version as follows:
public static RemoteStub exportObject(Remote obj) throws RemoteException
This method does not take any port number as arguments and expects existence of pre-generated class files for stub and skeleton. The name of the stub class is determined by concatenating the binary name of the remote object’s class with the suffix “_Stub”. Indeed, rmic compiler generates class files using the aforesaid naming convention. For example, the following command generates a class file simpieCaicuiator_stub.ciass from an implementation class file
SimpleCalculator.class.
rmic SimpleCalculator
If the exportObject() method does not find an appropriate stub class or is unable to load the stub class, or a problem occurs creating the stub instance, a stubNotFoundException is thrown.
To understand the functionality of this stub class, source code may be generated using any suitable Java de-compiler. A sample source generated using DJ Java de-compiler is shown below:
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.rmi.UnexpectedException;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteRef;
import java.rmi.server.RemoteStub;
public final class SimpleCalculator_Stub extends RemoteStub implements Calculator
{
private static final long serialVersionUID = 2L;
private static Method $method_add_0;
static
{
try
{
$method_add_0 = Calculator.class.getMethod(”add”, new Class[]
{
Integer.TYPE, Integer.TYPE });
}
catch (NoSEchMethodException localNoSEchMethodException)
{
throw new NoSuchMethodError(”stub class initialization failed”);
}
}
public SimpleCalcElator_StEb(RemoteRef paramRemoteRef)
{
super(paramEemoteEef);
}
public int add(int paramlntl, int paramInt2) throws RemoteException
{
try
{
Object localObject = this.ref.invoke(this, $method_add_0, new Object[]
{
new Integer(paramIntl), new Integer(paramInt2) },
-7734458262622125146L);
return ((Integer)localObject).intValue();
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (RemoteException localRemoteException)
{
throw localRemoteException;
}
catch (Exception localException)
{
throw new UnexpectedException(”undeclared checked exception”, localException);
}
}
}
So, if a programmer faces difficulty in generating a stub class (e.g. rmic compiler or implementation class is not available etc.), the programmer may use the previous version of exportObject() method without any trouble. We also have used this version to avoid generating stub class explicitly. In this case, Java RMI generates a proxy class (having a name like Proxy0) whose functionality is very much similar to the stub class generated by the rmic. It then creates and installs a proxy (skeleton) dynamically for the calculator object. The method exportObject() also creates and returns another proxy (stub) for this object to be uploaded to the object registry. We wrote a Java agent program to find the class data and using a DJ Java de-compiler a source was generated as follows:
import java.lang.reflect.*;
import java.rmi.RemoteException;
public final class $Proxy0 extends Proxy implements Calculator
{
public $Proxy0(InvocationHandler invocationhandler){ super(invocationhandler);
}
public final boolean equals(Object obj)
{
try {
return ((Boolean)super.h.invoke(this, m1, new Object[]
{
obj })).booleanValue();
}catch(Error _ex) { }
catch(Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
public final int add(int i, int j) throws RemoteException {
try {
return ((Integer)super.h.invoke(this, m3, new Object[] {
Integer.valueOf(i), Integer.valueOf(j)})).intValue();
}catch(Error _ex) { }
catch(Throwable throwable) {
throw new UndeclaredThrowableException^hrowable);
}
}
public final String toString() { try {
return (String)super.h.invoke(this, m2, null);
}catch(Error _ex) { }
catch(Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
private static Method m1; private static Method m0;
private static Method m3; private static Method m2;
static {
try {
m1 = Class.forName(”java.lang.Object”).getMetVod(”equals”, new Class[] {
Class.forName(”java.lang.Object”) }); m0 =
Class.forName(”java.lang.Object”).getMetVod(”VasVCode”, new Class[0]);
m3 = Class.forName(”Calculator”).getMetVod(”add”, new Class[] {
Integer.TYPE, Integer.TYPE });
m2 = Class.forName(”java.lang.Object”).getMetVod(”toString”, new Class[0]);
}catcV(NoSucVMetVodException nosuchmethodexception) {
throw new NoSucVMetVodError(nosucVmetVodexception.getMessage());
}
catch(ClassNotFoundException classnotfoundexception) {
throw new NoClassDefFoundError(classnotfoundexception.getMessage());
}
}
}
Java RMI generates this proxy class using the concept of dynamic proxy. It may be noted that the implementation of add() method in this class is similar to that of in the stub class generated by rmic compiler.
3.3. Registering the stub
The object’s server-side proxy (skeleton) is now ready to accept the incoming method invocation request. The object’s stub is also generated. This stub can communicate with the skeleton remotely using socket. So, if we can somehow create an instance of this stub in a remote computer, invoking a method on the remote object through that stub instance will be a matter of time. The stub is obtained from a separate application called registry where the server registers the stub.
To register the stub generated by exportObject() method with the object registry, a reference to object registry is needed. A new object registry may be created or an existing one may be used. The class java.rmi.registry.LocateRegistry provides many static methods for this purpose. In our example, we shall use an existing object registry which can be started using rmiregistry application provided by JVM. The rmiregistry application runs as a separate process and allows applications to register remote objects or obtain references to named remote objects. Java RMI only allows us to run rmiregistry and server in the same computer currently.
The following code is used to get a reference to an existing object registry which is already started on the same computer on default port (1099).
Registry registry = LocateRegistry.getRegistry();
Specify the port number to the getRegistry() method, if object registry runs on a port other than 1099. Instead of using an existing registry, the application itself may explicitly create a registry as follows:
Registry registry = LocateRegistry.createRegistry(1099);
This creates a registry that listens on port 1099. Note that a registry created using the above piece of code is available provided that the server application is started. Anyway, once we have a reference to the registry, the stub is registered with this object registry as follows:
String name = “calculator”;
registry.rebind(name, stub);
The rebind() method essentially binds a specified stub and a name. If there is an existing binding for the specified name, it is overridden. So, the stub for the calculator object is hereafter known as ‘calculator’.
When a client requests the object registry to have a reference to the remote object, the stub for the remote object is passed. The client-side application that contacted the object registry creates and installs an instance of this stub in the client’s computer and returns a reference to the client. Client basically invokes methods on this local stub object. As mentioned earlier, this stub has complete information about the skeleton, which is object’s server-side proxy. The stub creates a socket to the skeleton. It packs the information such as the method name to be invoked, parameters etc. This is called marshalling. The stub then sends the packed data to the skeleton through the socket. The skeleton then un-marshals the data and follows the steps as mentioned previously. The entire execution sequence is shown in Figure 14.6: .
The entire piece of code is embedded in a try-catch block.
The source code for server is stored in the file CalculatorServer.java. The source code for the CalculatorServer.java class is given as follows:
//CalculatorServer.java
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
public class CalculatorServer {
public static void main(String args[]) { try {
SimpleCalculator cal = new SimpleCalculator();
Calculator stub = (Calculator)UnicastRemoteObject.exportObject(cal,0);
Registry registry = LocateRegistry.getRegistry();
String name = “calculator”;
registry.rebind(name, stub);
System.out.println(”Calculator server ready…”);
}catch (Exception e) { e.printStackTrace(); }
}
}
4. Writing an RMI Client
Clients for SimpleCalculator are relatively simple. It is the program that invokes add() method on the object created and exported by the server. However, to invoke a method, it has to obtain a reference to the remote object first. A reference to a remote object may be obtained using:
- Registry
- RMI’s naming service
- Passing and returning remote objects
In the current application, we shall use the first method. Note that remote references are stored in the object registry. So, client first synthesizes a remote reference to the object registry running on the server host using the LocateRegistry.getRegistry() method as follows:
Registry registry = LocateRegistry.getRegistry(args[0]);
The argument to the getRegistry() method is the first command line argument args[0] which is the name or IP address of the computer where the registry runs on the default port (1099). If the registry runs on a port other than 1099, you specify the port as the second argument to the getRegistry() method. The getRegistry() method consults the remote registry (using socket) and essentially returns a local proxy registry (stub for registry which is an instance of Registryimpi_stub class). Note that this proxy is created and loaded dynamically upon downloading the RegistryImpl_Stub class data from the remote object registry. This proxy registry knows all the information of the remote registry (port and IP address) and capable of communicating (using socket) with the remote registry upon request. The information about the registry object (such as class file name) may be verified using the following code:
System.out.println(registry);
When this statement is executed, it generates an output shown below:
RegistryImpl_Stub[UnicastRef [liveRef:[endpoint:[172.16.5.81:1099](remote),objI
D:[0:0:0, 0]]]]
Once the client gets a reference to the remote registry, it uses the lookup() method on this registry to get a reference to the remote calculator object as follows:
String name = “calculator”;
Calculator cal = (Calculator)registry.lookup(name);
Note that the client uses the same name that the server uses to register the object. The lookup method of the proxy registry consults the remote registry, downloads the stub for the SimpleCalculator object having the name “calculator” specified as argument. It then creates and installs an instance of the stub (dynamically using the concept of dynamic class loading) and returns a reference to the client. The reference cal is actually a local reference to the stub. The information about the cal object (such as class file name) may be verified using the following code:
System.out.println(cal);
When this statement is executed, it generates an output shown below:
Proxy[Calculator,RemoteObjectInvocationHandler[UnicastRef [liveRef:
[endpoint:[172.16.5.81:52704](remote),objID:[-1812e356:1409d599c42:-7fff,
150949126975901178
6]]]]]
Note that, [172.16.5.81:52704] is the socket address of the skeleton. This stub sits behind the scene and behaves as a proxy of the remote calculator object. When client invokes a method using the cal reference, a similar method invocation on the stub occurs. The stub then creates a TCP socket connection with the skeleton, marshals the method invocation information and sends the request. When the result comes back from the skeleton, the stub returns the result to the client. The underlying procedure is transparent to the client.
Invoking the add() method on the remote object is now as simple as invoking a method on an ordinary local object.
int x = 4, y = 3;
int result = cal.add(x,y);
Here is the complete source code for the client which is stored in CalculatorClient.java.
//CalculatorClient.java import java.rmi.*;
import java.rmi.registry.*;
public class CalculatorClient
{
public static void main(String args[])
{
try {
String name = “calculator”;
Registry registry = LocateRegistry.getRegistry(args[0]);
//System.out.println(registry);
//uncomment above line if you want to display the info. about registry
Calculator cal = (Calculator)registry.lookup(name);
//System.out.println(cal);
//uncomment above line if you want to display the info. about cal
int x = 4, y = 3;
int result = cal.add(x,y);
System.out.println(”Sent: ” + x +” and ”+y);
System.out.print(”Received(”+x+”+”+y+”=): ” + result);
}catch (Exception e) { e.printStackTrace(); }
}
}
Source: Uttam Kumar Roy (2015), Advanced Java programming, Oxford University Press.