Cliff, vice president of technology of Digital Focus, can be contacted at [email protected] To submit questions, check out the Java Developer FAQ web site at http://www.digitalfocus.com/faq/.
A distributed application is one in which cooperating functions or objects exist on multiple remote nodes within a network. A distributed Java application may involve components that exist on multiple servers, communicating through a protocol such as Java Remote Method Invocation (RMI). In many such applications, programs requesting a service from another node will know ahead of time what service they need, and calls to the service can be encoded directly into the calling program's code. In other situations, the client of a remote service may not know in advance what services it needs, or where those services may reside; therefore, it needs to have a way to dynamically query servers for a kind of service, and possibly even ask for the details of the call interface for the service.
For example, a client application might want to find a list of all services available on node "abc.somewhere.com," and present the list to users for selection. Upon selection, the application would have to find out how to invoke the service (determine what parameters it takes), allow users to enter values for those parameters, and then invoke the service. A client program that performs this function is a "remote object browser." The capability to introspect and invoke remote objects is something provided by the CORBA Dynamic Invocation Interface (DII). This can also be done with Java RMI, and in a more powerful way.
One difference between CORBA DII and Java RMI is that all services invoked with DII execute on the remote server. The DII mechanism provides a client with enough information to pass arguments to a remote object and invoke the remote methods declared in its interface. The code remains on the server. RMI provides a more flexible mechanism, because object classes can be dynamically retrieved and invoked on the client. For example, an object returned by a remote call can either be or create an instance of a class not present on the client. The RMI mechanism can automatically retrieve the class from the server. The client can then call any method of this dynamically retrieved class. This month, I'll demonstrate the dynamic class-loading feature by creating a remote object browser that uses RMI.
To create a remote object service with RMI, you first define a remote interface, which client applications use to make remote calls to your object. The RMI interface then becomes part of the executable content of the application.
Once the remote interface is defined, you write a server class that implements the interface. This implementation executes on the server whenever a client makes a remote call to your server object. The server class is then input to an "rmic" compiler, which generates code that connects the client and server components with the underlying remote call mechanism. The server portion of this glue code is called the "skeleton," and the client-side portion is the "stub." The stub is a proxy class that implements your remote interface and is type-compatible with the server class, which also implements the interface. Your client code can therefore make calls to the stub as if it were making calls directly to the server class. Underneath, the stub converts the passed parameters into a reconstructable stream of data, and sends the data to the server-side skeleton. The data is reconstructed and passed to a real call to the designated method in the server object.
A server-side program generally consists of a main class that creates an instance of a server class, and then "binds" that instance to a name, via a naming service called the "RMI Registry." This registry service provides a way for remote clients to look up objects that are running on that machine. They do this by calling a lookup method and passing the name of the service -- which must be identical to the name used to bind the service. The lookup method returns a stub object, which can then be used to make remote calls. Since a remote call on one remote object can return another remote (stub) object, you usually don't have to do further lookups within the service you're accessing once you have looked up a server object.
Bootstrapping the Client
Bootstrapping is a technique for obtaining the class for an object that you do not yet have an instance of. To make a call to a remote method, you normally must have the remote interface that the remote object implements. Otherwise, assuming you have obtained a remote object via a call to the lookup method, your code will not be able to execute a statement such as ((MyInterface)remoteObject).remoteFunction();.
You need the interface to cast the remote object reference (really a stub) to a type that has a function called remoteFunction. If the application is an applet that was retrieved from a web server, the web server will obtain this interface from the web server the same way it obtained the applet. If, however, your main program is not an applet, the main program class will have been loaded from the local file system, and there will be no codebase for the current class. ((MyInterface)remoteObject).remoteFunction(); will fail with a NoClassDefFoundError. Furthermore, there is no way for the RMI mechanism to load the interface from the remote server that it is currently connected to, since the cast operation is not a member function and is resolved in the context of the calling class -- your main class.
One solution might be to explicitly load the interface. For example, you could get the RMI class loader (which knows the location of the classes), then attempt to load the interface with a statement such as remoteObject.getClassLoader().loadClass("MyInterface");. This will load the interface, but still will not let you do the cast. The cast operation that you need to do appears in your main class, which was loaded from your local system; it therefore has no class loader, nor will it be able to find the remote classes or interfaces that it may need for class resolution.
A better solution is to find a way to invoke a method in your remotely retrieved object, without needing its class. You can do this by designing the remote class to implement an interface that can be locally resolved. For example, the Runnable interface (part of package java.lang, and available on every client) has a single method called run. If your remote object implements this interface, you can simply get a remote instance of the object, then call its run() method. From then on, the object will know how to find any classes it needs because its class was loaded with a class loader (the RMI class loader); see Example 1.
This technique is suitable for arbitrary applications that cannot run as applets, use RMI, and must be automatically deployed. All you need is a bootstrap program on the client, which can be generic in nature. Thus, the client is nothing more than a factory that knows how to retrieve remote objects and start them. But what if you want to do more than that, like invoke arbitrary methods on those objects -- not just a run() method?
RemoteObjectBrowser, the program I present here, lets you select a server and browse the RMI objects registered on that server. The client does not have to be aware of objects prior to their discovery and invocation by the browser. The browser can execute as an application -- all the server-side classes it needs to perform its remote invocations are dynamically downloaded using the RMI class loader. The source code for RemoteObjectBrowser is available electronically from DDJ (see "Availability," page 3) and http://www.digitalfocus.com/.
Once you select a remote server, the object browser lets you click on a remote object and dynamically discover the remote methods implemented by that object. Then, you can click on any method, and a dialog will come up that lets you enter parameters for that remote method and invoke the method dynamically. The result is displayed in a field in the dialog.
Figure 1 shows the browser. The main window has a field for entering a remote host and a Connect button. It also has two subwindows. Both subwindows are of type TableArea, a utility class I have defined that adds row selection capability to a TextArea. TableArea does this by algorithmically correlating the TextArea character clicked on by the mouse with the row number that the character exists in. The row number is returned as an argument when constructing the event that the TableArea broadcasts to its action listeners. Thus, all action listeners to the table receive the row number that was selected.
The remote object browser's Connect button causes the connect() method to be called. The connect() method finds the registry on the remote host specified by the user. It then gets a list of remote objects registered with that registry, and displays them in the object lister GUI component. Users can select one of these objects. A click in the object subwindow results in a row selection, and a broadcast of an action event to the object subwindow's listeners (that is, the object browser) via a callback to actionPerformed().
The browser's actionPerformed() method tests which GUI component was clicked on: If it was the object lister subwindow, the getActionCommand() method is called on the event object passed into actionPerformed(). This method returns the row number argument -- the row selected. The object browser then associates this row with the correct entry in the list of remote object names displayed, based on ordinal position. The browser responds to the object selection by displaying the remote object's methods in the method subwindow.
To do this, it first gets this information about the remote object. It does this by calling remoteObject = (Remote)(remoteRegistry.lookup(objectName)); -- an interesting call, because the object returned by this call is actually an instance of the RMI stub for the remote object. The client program does not have the stub class present, since it presumably has never encountered this object type before. The RMI class loader downloads the stub class, using a URL for the stub instance encoded in the RMI object stream. (For example, the object stream tells the client where it can get the object's class definition.)
You then get the class that has been downloaded (currentClass = remoteObject.getClass();) so that you can perform reflection analysis on that class and determine its methods. My method for doing this, getMethods(), takes a Boolean parameter, indicating whether you want to discover all methods for the class (which is a stub class that implements the remote object interface) or just remote methods. You should be interested mostly in the remote methods, and not in calling the stub methods, but the ability to list the stub methods is included here for completeness. In fact, the remote object browser has a checkbox for indicating whether you want to list only the remote methods, or the stub methods aw well.
The getMethods() method calls currentClass.getMethods(), which returns an array of Method objects, each describing a method for the class. You add each of these method objects to your list of methods, which are then displayed in the method lister GUI component. If users have selected to show only remote methods, I scan through all the interfaces implemented by the stub. If the method does not appear in an interface that implements java.rmi.Remote, I don't include it.
When a method is selected from the method lister GUI component, an action event is generated and sent to the remote-object browser, in a similar fashion as for the object lister, and the event object sent includes the row number selected. The actionPerformed() method in the remote-object browser checks if the source of the event was the method lister, and if so, determines which method was selected based on the row number. Once the method selected is determined, the remote object browser constructs and shows an instance of MethodDialog (which really extends Frame). The constructor for the method dialog gets the types of the parameters for the method by calling getParameterTypes() on the method object. It then constructs a panel for entering values for the parameters, remotely invoking the remote object, and displaying the return result.
The actual remote invocation occurs as a result of users clicking the Invoke button, which calls result = method.invoke(instance, parms);. The instance parameter to this call is the stub for the remote object. The parms parameter is an array of objects that contain values for the remote method's parameters. This invocation is local, because you are invoking a method on the stub object, which is a proxy for the real object located on a remote server. The stub marshalls the parameters and sends them to the actual object, via the RMI protocol. It then waits for a return value in the RMI stream, and reconstructs the returned object, to which the invoke call then returns a local reference.
The parameter list is constructed by parsing the values for parameters entered by the user on the method dialog panel. The panel displays the type of each parameter next to the field where users can enter its value. These types are obtained by calling the method object's getParameterTypes() method, which returns an array of Class objects, one for each of the remote method's parameters. If a parameter is an array type, the Class object returned is anonymous, and you must call the class's getComponentType() recursively until you find the base type of the array. I display an array parameter type as a sequence of "" -- one for each array index -- preceded by the base type.
To test the object browser, I provide a sample server program called "PingPongServer," which implements a remote interface called "PingPong" that has three methods: ping(), pong(), and bong(). Running this server lets you see if you can access its methods remotely with the remote object browser. The machine hosting the server program will have to have a web server running on it, so that the remote classes can be retrieved by the client as needed. (If you don't have a web server, you can download the Java Web Server from Sun's site, or, for testing, use a file URL.) Analogous to an applet security manager, the RMI security manager restricts downloads to the host to which the RMI connection exists. You can override this restriction by subclassing the RMISecurityManager class and overriding that security check. You must use a security manager because the RMI downloading mechanism will refuse to work if there is none.
The RMI registry will only encode a class's URLs in a returned object stream if the registry obtains the class via a URL. If the class is in its classpath, it will not encode the URL, and remote clients will not know where to download the object's class from. Thus, when you run the registry, you should make sure that only the JDK classes are in its classpath. (In particular, do not put "." in its classpath and don't run the registry from a directory containing any classes that will need to be transported to clients.)
When running your server program, set the codebase property for the program. When the server object registers itself with the registry, the registry uses the codebase property to find classes for that server object. To set the codebase property for PingPongServer, use java -Djava.rmi.server.codebase=http://myhost/mydir/ PingPongServer. If you prefer to test the program without a web server, use a file URL instead. On Windows NT, use a URL of the form "file:/c:\mydir\"; on UNIX, use "file:/mydir/".
The BeanInfo Alternative
RemoteObjectBrowser uses the Java Reflection API, which works for any Java object. However, reflection does not make available parameter names -- only their types. It would be nice to have a mechanism to publish descriptions of remote objects, accessible to remote object browsers. To accomplish this, you can use the BeanInfo API -- the mechanism used by JavaBeans that lets developer tools find out information about reusable components, so that they can be conveniently incorporated by developers into finished applications. The BeanInfo API lets component designers include information about a component (a "bean"), such as textual descriptions of methods and parameters, and parameter names.
Using this API for remote introspection requires that remote objects be implemented as beans. This requires adherence to conventions when naming remote objects. Tools which use beans normally obtain the BeanInfo class for a bean by appending "BeanInfo" to the name of the bean class. In this case, the bean is the remote server object, and the client doesn't know the name of the server object. Instead, it knows the name of the service that has been registered, and name of the stub class. To identify the BeanInfo class, it needs a convention for determining the name of the server object class -- or you can use a different convention for determining the name of the BeanInfo, which would not be recommended.
Finding the BeanInfo for a remote bean therefore requires a convention for the naming of server objects. A possible convention is to name the service identical to the server class. For example, if the server object class is OurServer, the server object would also be registered with the name OurServer.
Another problem is that the normal method for retrieving a BeanInfo object is to use the Introspector.getBeanInfo() method, which takes the bean class as a parameter -- but you don't have this class locally (and you should not need it) because it is the server class. You could either retrieve this class and call getBeanInfo(), or you could retrieve the BeanInfo class manually via BeanInfo beanInfo = remoteObject.getClass().getClassLoader().loadClass("OurServerBeanInfo");.
Thus, you first find the class loader that was used to download the remote object (the stub), then explicitly use it to load the specified class. This puts the BeanInfo class for the server object in the class namespace of all the other downloaded classes, and so it will be able to introspect on the bean (including any Method classes that may have already been retrieved), and fetch additional introspection classes as needed.
If you implement this approach, you'll find the BeanInfo object may have descriptions of the bean's methods, but the methods it points to are the wrong ones: The BeanInfo object will point to Method objects for the server object -- you have the proxy. So, while the method descriptions and parameter names are useful, you'll have to perform an association operation on the method descriptors to correlate BeanInfo method descriptions with stub methods. This isn't hard; you simply compare method signatures.
RemoteObjectBrowser uses primitive input field components for obtaining and parsing parameter input values entered by the user. A better approach would be to use the JavaBeans default property editors to provide input editors for the standard Java types.
Copyright © 1997, Dr. Dobb's Journal