home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Book Home Java Enterprise in a Nutshell Search this book

3.4. Accessing Remote Objects as a Client

Now that we've defined a remote object interface and its server implementation and generated the stub and skeleton classes that RMI uses to establish the link between the server object and the remote client, it's time to look at how you make your remote objects available to remote clients.

3.4.1. The Registry and Naming Services

The first remote object reference in an RMI distributed application is typically obtained through the RMI registry facility and the Naming interface. Every host that wants to export remote references to local Java objects must be running an RMI registry daemon of some kind. A registry daemon listens (on a particular port) for requests from remote clients for references to objects served on that host. The standard Sun Java SDK distribution provides an RMI registry daemon, rmiregistry. This utility simply creates a Registry object that listens to a specified port and then goes into a wait loop, waiting for local processes to register objects with it or for clients to connect and look up RMI objects in its registry. You start the registry daemon by running the rmiregistry command, with an optional argument that specifies a port to listen to:

objhost% rmiregistry 5000 &

Without the port argument, the RMI registry daemon listens on port 1099. Typically, you run the registry daemon in the background (i.e., put an & at the end of the command on a Unix system or run start rmiregistry [port] in a DOS window on a Windows system) or run it as a service at startup.

Once the RMI registry is running on a host, you can register remote objects with it using one of these classes: the java.rmi.registry.Registry interface, the java.rmi.registry.LocateRegistry class, or the java.rmi.Naming class.

A Registry object represents an interface to a local or remote RMI object registry. The bind() and rebind() methods can register an object with a name in the local registry, where the name for an object can be any unique string. If you try to bind() an object to a name that has already been used, the registry throws an AlreadyBoundException. If you think that an object may already be bound to the name you want to register, use the rebind() method instead. You can remove an object binding using the unbind() method. Note that these three methods (bind(), rebind(), and unbind()) can be called only by clients running on the same host as the registry. If a remote client attempts to call these methods, the client receives a java.rmi.AccessException. You can locate a particular object in the registry using the lookup() method, while list() returns the names of all the objects registered with the local registry. Note that only Remote objects can be bound to names in the Registry. Remote objects are capable of supporting remote references. Standard Java classes are not, so they can't be exported to remote clients through the Registry.

The LocateRegistry class provides a set of static methods a client can use to get references to local and remote registries, in the form of Registry objects. There are four versions of the static getRegistry() method, so that you can get a reference to either a local registry or a remote registry running on a particular host, listening to either the default port (1099) or a specified port. There's also a static createRegistry() method that takes a port number as an argument. This method starts a registry running within the current Java VM on the given local port and returns the Registry object it creates.

Using the LocateRegistry and Registry interfaces, we can register one of our ThisOrThatServerImpl remote objects on the local host with the following code:

ThisOrThatServerImpl server = new ThisOrThatServerImpl();
Registry localRegistry = LocateRegistry.getRegistry();
try {
  localRegistry.bind("TTServer", server);
}
catch (RemoteException re) { // Handle failed remote operation }
catch (AlreadyBoundException abe) { // Already one there }
catch (AccessException ae) { // Shouldn't happen, but... }

If this operation is successful (i.e., it doesn't raise any exceptions), the local registry has a ThisOrThatServerImpl remote object registered under the name "TTServer." Remote clients can now look up the object using a combination of the LocateRegistry and Registry interfaces, or take the simpler approach and use the Naming class.

The Naming class lets a client look up local and remote objects using a URL-like naming syntax. The URL of a registered RMI remote object is typically in the format shown in Figure 3-3. Notice that the only required element of the URL is the actual object name. The protocol defaults to rmi:, the hostname defaults to the local host, and the port number defaults to 1099. Note that the default Naming class provided with Sun's Java SDK accepts only the rmi: protocol on object URLs. If you attempt to use any other protocol, a java.net.MalformedURLException is thrown by the lookup() method.

If we have a client running on a remote host that wants to look up the ThisOrThatServerImpl we registered, and the ThisOrThatServerImpl object is running on a host named rmiremote.farley.org, the client can get a remote reference to the object with one line of code:

ThisOrThatServer rmtServer =
  (ThisOrThatServer)Naming.lookup("rmi://rmiremote.farley.org/TTServer");

If we have a client running on the same host as the ThisOrThatServerImpl object, the remote reference can be retrieved using the degenerate URL:

ThisOrThatServer rmtServer = (ThisOrThatServer)Naming.lookup("TTServer");
figure

Figure 3-3. Anatomy of an RMI object URL

Alternately, you can use the LocateRegistry and Registry interfaces to look up the same object, using an extra line of code to find the remote Registry through the LocateRegistry interface:

Registry rmtRegistry = LocateRegistry.getRegistry("rmiremote.farley.org");
ThisOrThatServer rmtServer =
	(ThisOrThatServer)rmtRegistry.lookup("TTServer");

When you look up objects through an actual Registry object, you don't have the option of using the URL syntax for the name, because you don't need it. The hostname and port of the remote host are specified when you locate the Registry through the LocateRegistry interface, and the RMI protocol is implied, so all you need is the registered name of the object. With the Naming class, you can reduce a remote object lookup to a single method call, but the name must now include the host, port number, and registered object name, bundled into a URL. Internally, the Naming object parses the host and port number from the URL for you, finds the remote Registry using the LocateRegistry interface, and asks the Registry for the remote object using the object name in the URL.

The principal use for the Registry and Naming classes in an RMI application is as a means to bootstrap your distributed application. A server process typically exports just a few key objects through its local RMI registry daemon. Clients look up these objects through the Naming facility to get remote references to them. Any other remote objects that need to be shared between the two processes can be exported through remote method calls.

3.4.2. Remote Method Arguments and Return Values

As I've already mentioned, a critical element of executing a remote method call is the marshalling and unmarshalling of the method arguments and, once the method has executed, the reverse marshalling and unmarshalling of the method's return value. RMI handles this process for you automatically, but you need to understand how different types of objects are transmitted from the method caller to the server object and back again and, more importantly, you need to know which types of objects can't be used in remote method calls at all.

When you call a method on a remote object, the arguments to the method have to be serializable. That is, they need to be primitive Java data types (like int, float, etc.) or Java objects that implement java.io.Serializable. The same restriction applies to the return value of the remote method. This restriction is enforced at runtime, when you actually make the remote method call, rather than at compile time, when you generate the stubs and skeletons using the rmic compiler.

The RMI stub/skeleton layer decides how to send method arguments and return values over the network, based on whether a particular object is Remote, Serializable, or neither:

  • If the object is a Remote object, a remote reference for the object is generated, and the reference is marshaled and sent to the remote process. The remote reference is received on the other end and converted into a stub for the original object. This process applies to both method arguments and return values.

  • If the object is Serializable but not Remote, the object is serialized and streamed to the remote process in byte form. The receiver converts the bytes into a copy of the original object.

  • If the method argument or return value is not serializable (i.e., it's not a primitive data type or an object that implements Serializable), the object can't be sent to the remote client, and a java.rmi.MarshalException is thrown.

The principal difference between remote and nonremote objects is that remote objects are sent by reference, while nonremote, serializable objects are sent by copy. In other words, a remote reference maintains a link to the original object it references, so changes can be made to the original object through the remote stub. If the server object calls update methods on an argument to a remote method, and you want the updates to be made on the original object on the client side, the argument needs to be a Remote object that automatically exports a stub to the server object. Similarly, if the return value of a remote method call is intended to be a reference to an object living on the server, the server implementation needs to ensure that the object returned is a Remote object.

3.4.3. Factory Classes

When a reference to a remote object is obtained through the RMI registry and then used to request additional remote references, the registered remote object is often referred to as a factory class.

Factory classes are useful in distributed applications that use remote objects because in most cases you can't predict beforehand the kind and number of remote objects that will need to be shared between two processes. To make a remote object visible to clients through the RMI registry service, you need to explicitly create the object inside a Java VM on the server and then register that object using the bind() or rebind() method on the Registry. Using remote references obtained through method calls on factory objects, however, the client application can dynamically request the creation of new remote objects, without the objects being registered individually with the server registry.

As an example, suppose we're building a remote banking system, using the Account object we saw earlier in the chapter. We want to set up a centralized server that provides account services to remote clients running on PCs, embedded in ATMs, etc. On the server, we could run an RMI registry, create an Account object for every account we have on record, and register each one with the RMI registry service using the account name. In this scheme, registering accounts with the RMI registry goes something like this:

Registry local = LocateRegistry.getRegistry();
local.bind("Abrams, John", new AccountImpl("John Abrams"));
local.bind("Barts, Homer", new AccountImpl("Homer Barts"));
  .
  .
  .

As you can imagine, this is quite unwieldy in practice. Starting the server can take a long time, as thousands of accounts need to be registered, many of them unnecessarily, since many accounts may not see any activity before the next downtime. More importantly, accounts that are created or closed during the server's lifetime somehow need to be added or removed from the RMI registry, as well as from the bank's database of accounts. A much more sensible approach is to define a factory class for Account objects, along the lines of the following interface:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface AccountManager extends Remote {
  public Account getAccount(String name) throws RemoteException;
  public boolean newAccount(Account s) throws RemoteException;
}

The AccountManager lets a client ask for an account by name, using the getAccount() remote method. The method returns a reference to an Account object that corresponds to the account. Once the client has the Account reference, transactions against the account can be done through method calls on the Account object. The AccountManager also has a newAccount() method that allows clients to add new accounts to the manager's underlying database.

The server implementation of the getAccount() method simply needs to look up the named account in the account database, create an AccountImpl object to represent the account, and return the object to the remote client as a remote reference. Since Account objects are Remote objects, the RMI remote reference layer automatically creates a remote reference for the Account object, and the client that called the getAccount() method receives a stub for the Account object on the server.

Using the factory object to find accounts is more manageable than using the RMI registry. The bank maintains a database of accounts and their status, so the server implementation of the AccountManager can access that database directly to find accounts and create corresponding Account remote objects. Trying to keep the RMI registry in sync with the bank database makes the registry an unnecessary shadow of the main database of accounts, giving the bank two databases to maintain.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.