Remote Method Invocation (RMI) |
COMP 310 Java Resources Eclipse Resources |
Remote Method Invocation is Java's "objectization" of the usual Remote Procedure Call capablilites of most modern server-capable operating systems such as Unix/Linux or Windows Server. RMI enables a client computer to access an object located in a remote server as if it were a locally instantiated object in the client.
Quick links:
Remote clients communicate with server objects via stubs and skeletons. These stubs and skeletons are automatically produced by the Java compiler on class files that extend UnicastRemoteObject and have an interface that extends Remote. The stubs and skeleton are implementations of the Proxy Design Pattern, which is related to the Decorator Design Pattern.
Factories are one of the most useful techniques to use with RMI. Usually the server only binds out one object, a factory object, which the client uses to instantiate all the other objects it actually needs. This makes connecting much simpler, more flexible and more robust.
This technique is important enough that Sun even has a whole
web page devoted to it: https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/Factory.html
When an object is serialized, Java inserts both the class name and a URL value, java.rmi.server.codebase, into the byte stream. The Comp310 RMIUtils.startRMI(portNumber) call will set the java.rmi.server.codebase value to refer to the class file server that it sets up running on your local (sending) machine on the portNumber port that can retrieve class files from your project's bin folder.
When an object is deserialized, the Java class loader looks at the imbedded class name and first checks whether or not the class is on the local (receiving) machine. If the class is not found there, then the class loader uses the imbedded java.rmi.server.codebase value to make an HTTP GET call (the same type of call a browser uses to retrieve a web page) to that URL to retrieve the class file.
The Java class loader is smart enough to realize that if an imbedded object uses other classes, then if those classes are not available locally, the class loader will then use the same java.rmi.server.codebase value to retrieve those classes as well from the remote location.
Sun (the originators of Java) has written a simple class file server that can be used for dynamic class loading. That code is provided here in a slightly modified version of Sun's class file server. It will work properly only if the java.rmi.server.codebase system property is as a URL that points to it. For example, http://foo.cs.rice.edu:2001. The server also needs to know the exact path to the default Java package directory on the server machine.
Note: If both the client and the server have classes with exactly the same name, errors (usually unmarshalling errors) can occur. It is highly recommended that the names of any serialized classes being sent across the RMI connection be "personalized" in some way--by appending one's initials to the classname for instance. This problem can be avoided by carefully setting up exactly what directories are visible to the RMIRegistry, class server and application when they are running, but that is beyond the scope of this web page. For this reason, Java has class naming conventions that, in a nutshell, are that any package create for Internet use should be their creator's URL backwards, e.g. edu.rice.cs.comp310.rmi.
security.policy
file.java.rmi.server.codebase
-- The IP address of
the RMI server objectjava.rmi.server.hostname
-- The IP address of the
class file server for remote dynamic class loadingjava.rmi.server.useCodebaseOnly
-- If false, remote
dynamic class loading is allowed.java.rmi.server.logCalls
-- Prints RMI interaction
information to (unfortunately) stderr.
Since the serialization process is a deep-copy process, where object references are followed and any referenced objects are serialized alongside the host object, attempting to serialize an anonymous inner class will cause Java to attempt to serialize the enclosing object(s) that comprise the anonymous inner class's closure. This is because all inner classes contain a hidden field that references the enclosing class(s) that form its closure. However, the enclosing class may not be serializable or not desired. For the most part, the enclosing class is either to big to serialize and transmit or simply not be serializable, thus throwing an error.
This is the #1 cause of serialization/marshalling/unmarshalling errors!
Work-arounds:
UPDATE: As of 12/2015 (Java 8), there is evidence that
void
returns are not being executed asyncronously. The Java RMI
specification is a bit vague on this issue but there is documention that
suggests that it must wait because non-declared run-time exceptions, e.g.
IllegalArgumentException
, must be passed back to the caller of a method
on a stub (https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-stubs24.html).
In light of this, it is highly recommended that in asychronous
message-passing architectures, that all method calls to RMI stubs be handled by
a dedicated thread that releases the caller from the waiting for the void
return to complete.
For methods that are frequently used, a dedicated
producer-consumer mechanism with a BlockingQueue
is
recommended.
RMI does not wait for the return of a remote void
method call to return
before proceeding on to the next statement. This can cause some strange
behaviors if the code assumes a certain order of execution based on the
assumption that the call to the void method waits for it to finish, as normally
happens in a non-RMI, single-threaded situation. For instance, the following
code can cause problems:
// in ObjectA, initially call method1() below. remoteObjectB is a RMI stub to a remote ObjectB instance. void method1() { remoteObjectB.doIt(); System.out.println("the statement after the call to remoteObjectB"); } void method2() { System.out.println("this might come out first or second!"); // This will mostly likely come out second due to network delays. // In a non-RMI, single-threaded situation, this line would print first. }
// The following is in class ObjectB which holds an RMI stub to the above ObjectA instance. void doIt() { remoteObjectA.method2(); }
This issue may be the source of common "Object not in table" or "Connection Refused" errors. Symptoms include inconsistent program behavior from execution to execution and operations that stop working after working initially when a program is run.
When one exports a Remote object to create a RMI stub, it is tempting to believe that the auto-generated skeleton object would maintain a reference to the original Remote object and thus prevent it from being garbage-collected but this is not so.
IMyRemoteClass stub = (IMyRemoteClass) UnicastRemoteObject.exportObject(myRemoteObject, aPort);
If for instance, 'myRemoteObject" above is an anonymous inner class defined right there, there is no reference to that object anywhere. The generated RMI skeleton does not keep a reference either so at some point the anonymous inner class object will get garbage collected. This means that an error will be generated the next time the stub is used because it has no more back-end object to actually perform the operations.
Solutions:
ALSO, always save the stub generated from an RMI Server object as a field somewhere because Java only allows you to generate a stub from an RMI Server object once but one's code typically needs to use, e.g. transmit, that stub many times.
Since RMI relies on I/O ports, one's Integrated Development Environment ("IDE", e.g. Eclipse, DrJava, etc) may not properly manage the I/O resources as your RMI application starts and stops, resulting in erratic behavior such as intermittent connection failures. Since any Java application is actually a process being run by the JVM, the host operating system is often not able to do the more complete "cleanup" that it does when a normal application terminates. If the IDE is also using RMI, such as DrJava uses to communicate with its Interaction Pane, there is the possibility that your RMI application may conflict with the RMI usage of the IDE.
Work-arounds:
When remote calls to methods on RMI skeletons (UnicastRemoteObject) are called, the RMI system starts up a new thread to handle that call. It is important to realize this, as it may cause some strange and unexpected problems:
Making changes to the GUI is a cross-thread invocation error: It is a cross-thread invocation error for the code that is running under the RMI processing thread to attempts to update or mutate the GUI in any manner. (Note: JTextArea.append() is one of the few thread-safe exceptions to this rule.).
A pathological but not-that-uncommon scenario:
Curious artifacts of the above problem:
A solution: In step 1 above, spawn a new thread that will actually perform the remote call and
Remote
interfaces
must be part of the shared API!It seems that RMI does not handle dynamic class loading of
Remote
interfaces very well. The problem arises when a
client receives the stubs implementing the same Remote
interface
from multiple sources. If the Remote
interface is not part of
the shared API, i.e. a common code base, then that interface must be dynamically
class loaded when the stub is deserialized. The problem is
that when stubs implementing the same dynamically loaded Remote
interface are received from multiple sources, RMI can't seem to tell that they
are actually implementing the same interface because it sees the
objects coming from different source computers. RMI then tries to
load the class file for that interface from each of the computers from which it
receives stubs, uniquely identifying each class associated with the
each of the downloaded class files.
Then, under certain circumstances, RMI realizes that it cannot
or rather, improperly identifies the class of a new incoming stub of that
Remote
interface. The result is a
ClassCastException
as RMI tries to cast the stub to one of the classes it
received from a different source. This is a very
confusing error because it appears to all aspects that RMI is unable to cast an
object to its own class because everything about the multiply-downloaded classes
all appear the same: fully-qualified name, methods, superclasses, implementing
interfaces, i.e. everything except the hash codes of the associated Class
objects (which is why RMI thinks they are different).
Bottom line: The Remote
interfaces used by the system MUST ALL be part
of the shared, public API.
Thus, the use of implementation-specific stub interfaces will not work.
The most common cause of this error is that an instance of the application is still running. Note that the Java RMI Registry and the class file server are technically separate processes and it is possible from them to keep running even if it looks like the main application has already shut down.
Always explicitly stop the RMI processes (IRMIUtils.stopRMI()
) and notify any connections when the application exits.
To figure out if there there are still application processes running:
To see what processes are using a particular address/port:
netstat -abo
If you know any of the commands or procedures for the missing items above, please let the course staff know right away so this page can be updated.
Unless you know exactly what the offending process is, it not recommended to outright kill it as this could cause data corruption problems. Try to exit the offending application gracefully or reboot one's computer if necessary.
Potentially problematic apps (please let the course staff know if you find any other apps so this list can be updated!):
© 2020 by Stephen Wong