Java RMI: Calling Methods Across the Network
Java's Remote Method Invocation lets you call methods on objects running in a different JVM, possibly on a different machine. Here is how it works and where it fits in distributed system design.
Java RMI: Calling Methods Across the Network
One of the things that makes Java interesting for distributed systems work is RMI — Remote Method Invocation. The idea is straightforward: you define an interface, implement it as a remote object on a server, and any Java client can call methods on it as if it were a local object. The network is mostly invisible to the caller.
We are exploring RMI at Motorola for components that need to communicate across our internal network, and I want to walk through how it actually works rather than just how the documentation says it works.
The Basic Model
RMI is built on the concept of stubs and skeletons. When you compile a remote class with rmic, it generates two extra classes:
- A stub that lives on the client side. It looks like the real object but underneath it serialises method calls and their arguments and sends them over a socket.
- A skeleton that lives on the server side. It receives the serialised calls, deserialises the arguments, calls the actual method on the real object and sends back the return value (also serialised).
The serialisation layer means method arguments must implement java.io.Serializable. If they do not, you get a runtime error. This is a constraint worth noting upfront.
Defining a Remote Interface
Every remote object must implement an interface that extends java.rmi.Remote. All methods in the interface must declare throws RemoteException because any remote call can fail for network reasons:
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface DeviceRegistry extends Remote {
List getDeviceNames() throws RemoteException;
DeviceStatus getStatus(String deviceName) throws RemoteException;
void registerDevice(String name, String ipAddress)
throws RemoteException;
}
Note: List and DeviceStatus must both be Serializable since they cross the network as serialised bytes.
import java.io.Serializable;
public class DeviceStatus implements Serializable {
public String name;
public String ipAddress;
public boolean isReachable;
public long lastSeen;
}
Implementing the Remote Object
The server-side implementation extends UnicastRemoteObject and implements the interface:
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
public class DeviceRegistryImpl
extends UnicastRemoteObject
implements DeviceRegistry {
private Hashtable devices = new Hashtable();
public DeviceRegistryImpl() throws RemoteException {
super();
}
public List getDeviceNames() throws RemoteException {
return new ArrayList(devices.keySet());
}
public DeviceStatus getStatus(String deviceName)
throws RemoteException {
return (DeviceStatus) devices.get(deviceName);
}
public void registerDevice(String name, String ipAddress)
throws RemoteException {
DeviceStatus status = new DeviceStatus();
status.name = name;
status.ipAddress = ipAddress;
status.isReachable = true;
status.lastSeen = System.currentTimeMillis();
devices.put(name, status);
}
}
The RMI Registry
Before clients can look up remote objects, the server registers them with the RMI registry — a simple name-to-remote-object lookup service:
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RegistryServer {
public static void main(String[] args) throws Exception {
// Start the registry on port 1099 (the default)
LocateRegistry.createRegistry(1099);
// Create the remote object
DeviceRegistryImpl registry = new DeviceRegistryImpl();
// Bind it under a name
Naming.rebind("//localhost/DeviceRegistry", registry);
System.out.println("DeviceRegistry server ready");
}
}
The Client
The client looks up the remote object by name and calls it:
import java.rmi.Naming;
import java.util.List;
public class RegistryClient {
public static void main(String[] args) throws Exception {
String serverHost = args[0];
// Look up the remote object
DeviceRegistry registry =
(DeviceRegistry) Naming.lookup(
"//" + serverHost + "/DeviceRegistry");
// This looks like a local method call
List names = registry.getDeviceNames();
for (int i = 0; i < names.size(); i++) {
String name = (String) names.get(i);
DeviceStatus status = registry.getStatus(name);
System.out.println(name + " -> " + status.ipAddress +
" (reachable: " + status.isReachable + ")");
}
}
}
To the client code, registry.getDeviceNames() looks like a normal method call. The fact that it involves TCP sockets and serialisation is invisible unless an exception is thrown.
Compile and Run
The rmic compiler generates the stub and skeleton classes:
javac DeviceRegistry.java DeviceStatus.java \
DeviceRegistryImpl.java RegistryServer.java RegistryClient.java
rmic DeviceRegistryImpl # generates DeviceRegistryImpl_Stub.class
# and DeviceRegistryImpl_Skel.class
The stub class needs to be on the client's classpath. You can either deploy it manually or use RMI's dynamic class loading — the server can specify a codebase URL and the client JVM downloads stubs it does not have locally. Dynamic class loading works well in controlled environments where you trust the codebase server.
What Can Go Wrong
RemoteException on any call. Unlike local methods, any RMI call can throw RemoteException — the server may be unreachable, the network may drop, the server JVM may have died. Your client code must handle this gracefully rather than treating it as an unexpected condition.
Serialisation mismatches. If the client and server have different versions of DeviceStatus.class, deserialisation fails. In practice this means you need to manage your shared interface classes carefully — both sides must agree on the class definitions. Setting serialVersionUID explicitly on your serialisable classes is good practice.
Security manager requirements. For dynamic class loading to work, the client must install a security manager (System.setSecurityManager(new RMISecurityManager())). Permissions in java.policy control what downloaded code is allowed to do. This is correct but adds deployment complexity.
Garbage collection across VMs. RMI implements distributed garbage collection. The registry holds a reference to your remote object but if no clients hold references, the object can be collected. For long-lived server objects this is fine. For objects with shorter lifetimes you need to understand the DGC lease mechanism.
Where RMI Makes Sense
RMI is a good fit when:
- Both client and server are Java
- You want a clean Java interface as the contract, not a wire format
- You are building internal systems where you control both ends
- The argument and return types map naturally to serialisable Java objects
It is less suited to interoperability with non-Java systems — for that you want CORBA's IIOP, which RMI can actually be configured to use (RMI-IIOP is in development). And for systems where the communication contract needs to be language-neutral and version-stable, CORBA IDL gives you more explicit control.
But for Java-to-Java distributed systems where you want the simplest possible API, RMI is remarkably clean. The fact that calling a method on a remote object looks identical to calling a local one is a genuine quality of life improvement over raw socket programming.