Most tutorials and books on RMI suggest you create the Door
interface and the DoorImpl
class in this way, so that Door
extends java.rmi.Remote
, and DoorImpl
extends java.rmi.server.UnicastRemoteObject
and implements Door
. To add a smart proxy for DoorImpl
, however, you need to create a class that implements java.io.Serializable
, and also implements Door
, without also implementing java.rmi.Remote
. However, since Door
extends java.rmi.Remote
, that is impossible.
Factoring out the remoteness
What is needed is a way to separate the server object's interface from its remoteness. The solution depends on the fact that Java, while it doesn't allow multiple inheritance of classes, does allow interfaces to extend more than one parent interface. Therefore, you can refactor the design for Door
and DoorImpl
to look like this:
Notice that Door
does not extend java.rmi.Remote
. Instead, I've added a new interface, DoorRemote
, that extends both Door
and java.rmi.Remote
. DoorImpl
implements that new interface.
The following code shows the implementation of the new design:
/** * Define the Door interface. * @author M. Jeff Wilson * @version 1.1 */ public interface Door /* don't extend java.rmi.Remote */ { String getLocation() throws java.rmi.RemoteException; boolean isOpen() throws java.rmi.RemoteException; } /** * Add the 'remoteness' to Door. Notice that you don't have to * redeclare the methods in interface Door, just inherit both * from Door and java.rmi.Remote. * @author M. Jeff Wilson * @version 1.0 */ public interface DoorRemote extends java.rmi.Remote, Door { } /** * Define the remote object that implements the Door interface. * @author M. Jeff Wilson * @version 1.1 */ public class DoorImpl extends java.rmi.server.UnicastRemoteObject implements DoorRemote { private final String name; private boolean open = false; public DoorImpl(String name) throws java.rmi.RemoteException { super(); this.name = name; } // in this implementation, each Door's name is the same as its location. // we're also assuming the location will be unique public String getLocation() { return name; } public boolean isOpen() { return open; } // assume the server side can call this method to set the // state of this door at any time void setOpen(boolean open) { this.open = open; } // convenience method for server code String getName() { return name; } // override various Object utility methods public String toString() { return "DoorImpl:["+ name +"]"; } // DoorImpls are equivalent if they are in the same location public boolean equals(Object obj) { if (obj instanceof DoorImpl) { DoorImpl other = (DoorImpl)obj; return name.equals(other.name); } return false; } public int hashCode() { return toString().hashCode(); } }
Defining the proxy
Now Door
is not a remote interface (that is, it doesn't extend java.rmi.Remote
). The remoteness is added by the DoorRemote
interface, since it extends both Door
and java.rmi.Remote
. The semantics and behavior of DoorImpl
haven't changed; when DoorServer.getDoor(String)
is called, the RMI stub for DoorImpl
is returned to the client. But splitting the interfaces that way lets you add a new class that implements Door
, is serializable, but isn't remote. Let's add that class and call it DoorProxy
.
/** * Define a proxy for Door. Currently, the implementation of Door's * methods are stubbed out. * @author M. Jeff Wilson * @version 1.0 */ public class DoorProxy implements java.io.Serializable, Door { public String getLocation() throws java.rmi.RemoteException { return null; } public boolean isOpen() throws java.rmi.RemoteException { return false; } }
Since DoorProxy
implements java.io.Serializable
, a remote call can return a copy of it. DoorProxy
also implements Door
, so it appears the same to a client as a remote reference to a DoorImpl
object.
For DoorProxy
to be a true proxy, however, it needs to be able to store a reference to another object -- in this case, DoorImpl
-- and forward method calls to the reference. You can easily accomplish this:
/** * Define a proxy for Door. In this version, all methods are * delegated to the remote object. * @author M. Jeff Wilson * @version 1.1 */ public class DoorProxy implements java.io.Serializable, Door { // store a copy of the remote interface to a DoorImpl private DoorRemote impl = null; /** * Construct a DoorProxy. * @param impl - the remote reference to delegate to. */ DoorProxy(DoorRemote impl) { this.impl = impl; } public String getLocation() throws java.rmi.RemoteException { // delegate to impl return impl.getLocation(); } public boolean isOpen() throws java.rmi.RemoteException { // delegate to impl return impl.isOpen(); } }
A constructor has been added to DoorProxy
to ensure that every instance created has a reference to a DoorRemote
object. That reference will really be an instance of DoorImpl
but, since the client doesn't need to know about anything more than the remote interface (DoorRemote
), the details of DoorImpl
are hidden from it. (It's also necessary to get the code to work because, when RMI serializes a DoorProxy
, it's going to instantiate an RMI stub and replace the reference to DoorImpl
with a reference to that stub. To do that, the impl
field must be able to hold either a DoorImpl
or its stub.)
Now if the client makes a remote method call that returns a DoorProxy
, the DoorProxy
's data is serialized and a copy of the object is instantiated in the client VM. As RMI serializes the DoorProxy
, it notices that the impl
field is a reference to an object that should be replaced by its RMI stub, so it replaces it. The result is that the DoorProxy
instantiated in the client contains a remote reference to a DoorImpl
object and can delegate its calls via RMI to that server object.
Now that you have the smart proxy, you need to make sure there's a way the client can get one. The easiest way to accomplish that is to change DoorServerImpl.getDoor(String)
to return a properly constructed DoorProxy
instead of a DoorImpl
:
/** * Define the class to implement the DoorServer interface. * @author M. Jeff Wilson * @version 1.1 */ public class DoorServerImpl extends java.rmi.server.UnicastRemoteObject implements DoorServer { /** * HashMap used to store instances of DoorImpl. The map will be keyed * by each DoorImpl's name attribute, so it is implied that two Doors * with the same name are equivalent. */ private java.util.Hashtable hash = new java.util.Hashtable(); public DoorServerImpl() throws java.rmi.RemoteException { // add a door to the hashmap DoorImpl impl = new DoorImpl("location1"); hash.put(impl.getName(), impl); } /** * Changed to return the proxy. * @param location - String value of the Door's location * @return an object that implements Door, given the location */ public Door getDoor (String location) { DoorImpl impl = (DoorImpl)hash.get(location); return new DoorProxy(impl); } /** * Bootstrap the server by creating an instance of DoorServer and * binding its name in the RMI registry. */ public static void main(String[] args) { System.setSecurityManager(new java.rmi.RMISecurityManager()); // make the remote object available to clients try { DoorServerImpl server = new DoorServerImpl(); java.rmi.Naming.rebind("rmi://host/DoorServer", server); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } }
The client code doesn't change; it still requests and gets a reference to an object that implements Door
. Originally, it got the RMI stub to a DoorImpl
object; now it gets a DoorProxy
object. But since the client doesn't know about anything other than the Door
interface, that substitution is invisible to it.
Making the proxy smart
Looking at DoorImpl
, it's obvious that the name field will never change for a given instance, as it has been declared final
. If it's not going to change, why pay the cost of a network call on every invocation? A better solution would be to cache that field in the proxy itself, so that a call to DoorProxy.getLocation()
is a local call:
/** * Define a proxy for Door. In this version, the name field * is cached in the proxy. * @author M. Jeff Wilson * @version 1.2 */ public class DoorProxy implements java.io.Serializable, Door { // store a copy of the remote interface to a DoorImpl private DoorRemote impl = null; private final String name; /** * Construct a DoorProxy. * @param impl - the remote reference to delegate to. */ DoorProxy(DoorRemote impl) throws java.rmi.RemoteException { this.impl = impl; this.name = impl.getLocation(); } public String getLocation() throws java.rmi.RemoteException { // return the cached value return name; } public boolean isOpen() throws java.rmi.RemoteException { // delegate to impl return impl.isOpen(); } }
Likewise, you could change the behavior of any DoorProxy
method, without affecting any client code.
Making the proxy efficient
Since a copy of the DoorProxy
object is returned from DoorServerImpl.getDoor(String)
, a given client could quite possibly have several DoorProxy
copies that hold remote references to the same DoorImpl
object. That may not be desirable -- at the very least, it is a waste of memory in the client VM.
The same problem exists with normal RMI remote objects: the client can get more than one RMI stub to the same server object. The designers of RMI solved that problem by overriding Object.equals(Object)
and Object.hashCode()
in the stub classes generated by the RMI compiler (rmic). Those methods have been overridden so that two different stub instances that refer to the same remote object are equivalent. That is, if stubs objA
and objB
refer to the same remote object, then objA.equals(objB)
returns true
and objA.hashCode()
returns the same value as objB.hashCode()
. Once that is done, you can see if two instances of a stub are equivalent, discarding one if they are. Another approach would be to store each remote reference in a hashtable; if an equivalent stub is put into the table, it will replace the original copy.
You can do the same thing with smart proxies. In the DoorProxy
code, you could define Object.equals(Object)
and Object.hashCode
in the same way you defined those methods in the DoorImpl
class. However, a more general solution would be to copy what the RMI designers did and make sure that two smart proxies that delegate to the same remote object are equivalent. Since you have access to the RMI stubs, that becomes a trivial delegation:
/** * Define a proxy for Door. In this version, the name field * is cached in the proxy, and I've overridden equals() and * hashCode(). * @author M. Jeff Wilson * @version 1.3 */ public class DoorProxy implements java.io.Serializable, Door { // store a copy of the remote interface to a DoorImpl private DoorRemote impl = null; private final String name; /** * Construct a DoorProxy. * @param impl - the remote reference to delegate to. */ DoorProxy(DoorRemote impl) throws java.rmi.RemoteException { this.impl = impl; name = impl.getLocation(); } public String getLocation() throws java.rmi.RemoteException { // return the cached value return name; } public boolean isOpen() throws java.rmi.RemoteException { // delegate to impl return impl.isOpen(); } public boolean equals(Object obj) { if (obj instanceof DoorProxy) { return impl.equals(((DoorProxy)obj).impl); } return false; } public int hashCode() { return impl.hashCode(); } }
A complete example
I've taken the Door
and DoorServer
examples above and turned them into complete server and client applications. The .class
files and the source code are both in the zip file found in Resources. In that example, the DoorServerImpl
creates a hashtable of 100 DoorImpl
objects. The client connects to the DoorServerImpl
, gets all 100 Door
objects as remote references (RMI stubs), sorts them, and finally inserts them into a JList
component on the left-hand side of a window. Selecting a Door
in the list results in a call to Door.getLocation()
and Door.isOpen()
, the values of which are displayed in a panel on the right-hand side of the window. There's also a menu, Server, that lets you refresh the list with either RMI stubs (Get DoorRemote) or proxies (Get DoorProxy). Figure 5 illustrates what the client window looks like.
The DoorImpl
class has been changed to write a string to System.out
showing the total number of times getLocation()
has been called. DoorImpl
also starts a thread to randomly change the value of a Door
's isOpen
field once every second.
Run the example and watch the console window in which DoorServerImpl
was started. If the client is using the RMI stubs (the initial case), you will see a large number of remote calls to DoorImpl.getLocation()
, caused mostly by sorting the Door
objects. Scroll the list of Door
s, resize the window, or minimize and restore the window. Notice that every time the list needs to be updated more remote calls are generated.