Ubik RMI

Remoting for Dummies

Ubik RMI allows to distribute objects with minimum hassle. Especially, contrary to Java's RMI, remote interface methods do not have to throw remote exceptions, and stub compilation is abandoned (stubs are created dynamically with dynamic proxies). The former feature is of utmost interest: this means that no supplementary interfaces need to be defined only for the sake of remoting. As long as "application" interfaces extend the Remote interface, instances of these interfaces will automatically be treated as remote references.

Although seemingly a minor detail, the above point is of high value: indeed, in absolute terms, reducing the number of redundant, overlapping interfaces in an API reduces complexity; furthermore, the more there are interfaces, the more implementations there is, the more unit tests must be conducted and maintained, etc. In addition, the mere fact that intermediary interfaces have to be designed (and implementations provided) only for remoting introduces an overhead that reduces development time. With Ubik RMI, remoting is almost implicit (the only constraint is that application interfaces have to extend java.rmi.Remote); with the JDK's RMI, applications specifically have to define remote interfaces that must be implemented by classes that (to top it off) in addition must inherit from java.rmi.server.UnicastRemoteObject.

Some might argue that imposing RemoteExceptions guarantees robustness; fair enough: developers that want their remote interfaces to throw RemoteException have the freedom to do so. Ubik RMI is completely "backward" compatible with this (internally, Ubik RMI stubs throw these exceptions, which are translated to RuntimeException instances by the dynamic proxy layer if the exceptions are not declared in the "throws" clause of the called methods).

Yet, where do RemoteExceptions end up anyway? In the user interface, in most cases; usually, programmers will not provide any special business logic to handle RemoteExceptions. So what difference does it make that the remote interfaces throw RemoteExceptions or not? In our opinion, it only makes design more cumbersome.

The table below shows the minimal steps required to work with Ubik RMI (versus Java RMI):

Ubik RMIJava
Define an interface that extends java.rmi.Remote and some methods. Define an interface that extends java.rmi.Remote and some methods that throw RemoteException.
Write a class that implements the remote interface, or is annotated with the @Remote annotation. Write a class that implements the remote interface and extends java.rmi.server. UnicastRemoteObject.
Generated the above class' stub with Java's RMI compiler.

Note in the above that no stub compilation step is required in the case of Ubik RMI.

A well-designed application using Java RMI would decouple remote interfaces (and implementations thereof) from the "rest" of the application: the business logic, defined by "local" interfaces that have no knowledge of distribution issues, would symetrically be implemented by "local" classes. The remote interfaces (and corresponding remote classes) would correspond one-to-one with their local counterparts, the remote implementations serving as thin-wrappers around the local implementations.

Thus, the steps defined in the above table do not exactly correspond to reality, as far as Java RMI goes; the above is an oversimplification. One can see that in reality, rigourous use of Java RMI introduces a step that is completely unnecessary in the case of Ubik RMI: indeed, the transport layer in this case is totally abstracted from the application; Ubik RMI's runtime automically creates remote references based on the implementation of the Remote interface.

Seeing is Believing

The code snippets below illustrate how remoting in Ubik RMI works. The first step, has we have mentioned, consists of defining the remote interface:

package org.sapia.rmi.hello;

import java.rmi.Remote;

public interface Hello extends Remote{
        
  public String getMessage();

}

Next, implement the interface:

package org.sapia.rmi.hello;

public class HelloImpl implements Hello{
        
  public String getMessage(){
    return "Hello World";
  }

}

Now, export an instance of the above class as a server on a given port:

package org.sapia.rmi.hello;
import java.rmi.RemoteException
import org.sapia.ubik.rmi.server.Hub;

public class HelloImpl implements Hello{
        
  public String getMessage(){
    return "Hello World";
  }
  
  public static void main(String[] args){
    try{
      Hub.exportObject(new HelloImpl(), 7070);
      
      while(true){
        Thread.sleep(100000);
      }
    }catch(InterruptedException e){
      System.out.println("terminating");
    }catch(RemoteException e){
      e.printStackTrace();
    }
  }
}

For all Ubik socket-based transports other than HTTP, Ubik will attempt exporting remote objects using a server bound to the first network interface that it detects that does not correspond to localhost. If the host does not have such a network interface available, then Ubik resorts to localhost. If multiple network interfaces (other than localhost) are available on the host on which a Ubik server is started, then a regular expression can be used to indicate to Ubik which one to use. That regular expression must be specified as a System property, under the ubik.rmi.address-pattern key. If no address could be found that matches the given regexp, then Ubik also resorts to localhost. See the tutorial for more on this.

Note that our Hello interface in this case did not need to extend java.rmi.Remote; the Hub class "knows" that we want to remote-enable our instance (since we export it as a server), so it would internally generate a stub even if our object would not be an instance of Remote. As for methods that return remote objects, the Remote interface has to be extended by theses objects' interface - indeed, the Ubik RMI runtime has in this case no way to implicitely determine that a remote reference has to be returned. Note that instead of implementing the Remote interface, you could simply annotate your class with @Remote, which in addition would allow you to specify exactly which interfaces should be exported as part of the stub that Ubik eventually creates.

And now, of course, to make all of this meaningful, let's implement a client:

package org.sapia.rmi.hello;
import java.rmi.RemoteException
import org.sapia.ubik.rmi.server.Hub;

public class HelloClient {
        
  public static void main(String[] args){
    try{
      Hello h = (Hello)Hub.connect(7070);
      System.out.println(h.getMessage());
    }catch(RemoteException e){
      e.printStackTrace();
    }
  }
}