| ★ wanayoo — archive 1999 http://developer.java.sun.com/developer/technicalArticles/RMI/rmi/ | Nouvelle recherche | Portail wanayoo |
|
|
JavaTM RMI (remote method invocation) technology makes it very easy to create web-based applets that perform powerful server-side operations, such as accessing a database or communicating with remote server applications. However, a number of RMI limitations make its use in Internet-based applets impractical. This article describes a way to work around these limitations by using Java servlets and object serialization for applet/server communication, and demonstrates several ways to use this technique. The focus of this article is on converting an existing application, but the same design can used when creating new applications.
The Benefits of Using RMI in AppletsIn theory, choosing RMI as a means of communication between Java applets and server applications on the Internet is a great idea. The benefits of using RMI in such an application are persuasive, for the following reasons:
The Limitations of RMI Applets on the InternetAlthough using RMI in Internet-based applets seems like a good idea, in practice, the instability of Internet connections and a lack of client support make it impractical. Among the obstacles to deploying RMI applets on the Internet are the following:
Sample ApplicationIn this article you'll be working with a sample application: a simple web-based database client. The application consists of a Java applet that displays database values, and a server-side Java object that performs database tasks (add, remove, modify, and retrieve data) on behalf of the applet. Communication between the applet and the server object is via RMI.The following graphic illustrates the original architecture of the application:
The server side of this illustration is a Java application that does three things: it creates an RMI registry, creates an instance of the database access object (UserDatabaseImpl), and binds the object to the registry to make it available to remote clients. Here are the lines of code from the server application that accomplish these tasks:
// Create the registry on port 1099
Registry reg =
java.rmi.registry.LocateRegistry.createRegistry(
1099);
// Create the database object
UserDatabaseImpl db = new UserDatabaseImpl();
// Bind the object to the registry as "DB1"
java.rmi.Naming.rebind("DB1", db);
Once the database object is bound to the registry, your client applet can use java.rmi.Naming.lookup() to get a reference to it. Here's the client code:
UserDatabase db; //The remote database object ... String host = this.getCodeBase().getHost(); String name = "//" + host + "/DB1"; db = (UserDatabase)java.rmi.Naming.lookup(name);Once the applet has a reference to the remote object, it can execute database operations by invoking methods on this object. As with other RMI objects, your database object (UserDatabaseImpl) implements a remote interface (UserDatabase) that extends java.rmi.Remote. This remote interface defines the methods of the database object that can be executed by the client applet, and looks like this:
public interface UserDatabase
extends java.rmi.Remote
{
public String createUser(
UserData data) throws RemoteException;
public void editUser(
String userid, UserData data)
throws RemoteException;
public void deleteUser(
String userid) throws RemoteException;
public UserData getUser(
String userid) throws RemoteException;
public Hashtable listUsers(
) throws RemoteException;
}
To execute a database operation the client applet simply calls one of these methods on the remote object. For example, to get a list of the users currently in the database, the client applet executes:
Hashtable users = db.listUsers();
Overview of ChangesAlthough the above code snippets demonstrate the ease with which RMI allows you to create applets that access your database and perform other server-side tasks, you still want to eliminate it from the client side of your application because of the issues described above. In doing so, you have the following goals:
Once you complete the changes your application's architecture will look like this:
The ServletsThe servlet layer of your new architecture will contain a series of servlets that each executes a single method on your remote object. Here are the servlets you'll be making and the method ofUserDatabase they are associated with:
Because your servlets will each execute a single RMI call, they will be extremely simple. Each servlet will perform the same basic tasks:
java.rmi.Naming.lookup() just as the original applet code does.
Here then is the code for DatabaseServlet, your servlet super-class:
public class DatabaseServlet extends HttpServlet
{
String registry; // The path to the RMI
//registry
UserDatabase db; // The database access
//object
public void init(
ServletConfig config)
throws ServletException
{
super.init(config);
// Get the location of
//the RMI registry.
registry = config.getInitParameter(
"registry");
// If undefined, assume the local host
if (registry == null) registry =
"//localhost";
try {
// Now get the database object
String name = registry + "/DB1";
db = (UserDatabase)Naming.lookup(
name);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Now that you have your parent servlet you can create the individual servlets that extend this one. In doing so, you'll follow the recipe given above: read in parameter values, execute the database method, and return the result. All of these operations will occur in the servlet's doPost() method, which will also include the code required to open and close the I/O streams to the applet.
Here's your first servlet,
public class UserCreateServlet
extends DatabaseServlet
{
public void doPost(
HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
// Open the I/O streams
ObjectInputStream in = new
ObjectInputStream(
req.getInputStream() );
ObjectOutputStream out =
new ObjectOutputStream(
resp.getOutputStream() );
try {
// 1. Read in parameters from
//the applet
UserData data = (
UserData)in.readObject();
// 2. Execute the RMI call
String id = db.createUser(data);
// 3. Write the result
out.writeObject(id);
} catch (
Exception e) { e.printStackTrace(
); }
// Close the I/O streams
in.close();
out.close();
}
}
A couple of notes on this code:
UserGetServlet servlet, which executes the database's getUser() method, you simply change these lines to be:
// 1. Read in parameters from //the applet String id = (String)in.readObject(); // 2. Execute the RMI call UserData data = db.getUser(id); // 3. Write the result out.writeObject(data);In both of these servlets you've read in one object and written out one object, but you can read or write as many objects as you want. In the case of the database's editUser() method, you need to retrieve two parameters from the applet while returning none. In your applet that executes this method (UserEditServlet) you start with the same three-line template and simply add an extra readObject() line to get the second parameter, and remove the writeObject() line that writes the return value:
// 1. Read in parameters from
//the applet
String id = (String)in.readObject();
UserData data = (
UserData)in.readObject();
// 2. Execute the RMI call
db.editUser(id, data);
// 3. Write the result
// [no return value]
Your other servlets all follow the same format, so you won't print them all here Download code
Client Proxy ObjectNow that you have servlets that can execute your RMI methods via HTTP, your client applet needs to know how to use these servlets. Although you could rewrite every RMI method call in the applet to include the corresponding servlet calls, that could be a lot of changes to existing code. That's why you'll use a proxy object that duplicates the database object's syntax and does all of the necessary servlet communication; this lets you keep almost all of your current applet code in tact.
In creating your proxy object, String createUser(UserData data) void editUser( String userid, UserData data) void deleteUser(String userid) UserData getUser(String userid) Hashtable listUsers()As you did when creating your servlets, you'll following a simple recipe when defining each of these methods in your proxy object. In each of these methods you want to accomplish the following: Open a connection to the target servlet, write one or more parameters to the servlet, and read the result of the servlet execution and return it to the applet.
To help you accomplish this you'll be using a helper class called
URL webBase;// The URL to the servlet web server
public UserDatabaseProxy(URL web)
{
webBase = web;
}
Because the URL to the servlet web server is initialized during construction, each of your methods can create the URL to their corresponding servlets relative to this base URL.
With that out of the way, you can now write each of the proxy object's methods. You'll start with
public String createUser(
UserData data) throws Exception
{
// 1. Create a URL for the target
//servlet
URL servlet = new URL(
webBase,
"servlet/UserCreateServlet");
// 2. Create an array of parameter
//objects
Serializable objs[] = { data };
// 3. Execute the servlet
//transaction
ObjectInputStream in =
ServletWriter.postObjects(
servlet, objs);
// 4. Read and return the result value
String id = (String)in.readObject();
in.close();
return id;
}
As with your servlets, creating other methods in your proxy object requires mostly copying and pasting this first method. Here is the code for the getUser() method, which differs from the above method only in the name of the target servlet (UserGetServlet) and the type of the return object:
public UserData getUser(
String userid) throws Exception
{
// 1. Create a URL for the target servlet
URL servlet = new URL(webBase,
"servlet/UserGetServlet");
// 2. Create an array of parameter objects
Serializable objs[] = { userid };
// 3. Execute the servlet transaction
ObjectInputStream in =
ServletWriter.postObjects(
servlet, objs);
// 4. Read and return the result value
UserData data = (UserData)in.readObject();
in.close();
return data;
}
The other methods of your proxy object are similarly constructed, so you won't list them all here. UserDatabaseProxy.java
Applet ChangesOnce you have your proxy object and servlets in place, all you need to do is update your applet to use of the proxy object instead of the original RMI object. Here again is the original code for accessing the database:UserDatabase db; //The remote database object ... String host = this.getCodeBase().getHost(); String name = "//" + host + "/DB1"; db = (UserDatabase)Naming.lookup(name);Besides redefining db to be an instance of the proxy object instead of the RMI object, you need to create the URL for the servlet web server (required by UserDatabaseProxy's constructor). To do this you construct a URL from the protocol, host name, and port number of the applet's code base: UserDatabaseProxy db; // The proxy //database object URL codebase = this.getCodeBase(); // Get the host, protocol, and port String host = codebase.getHost(); String protocol = codebase.getProtocol(); int port = codebase.getPort(); // Build the URL for the servlet //web server URL servletBase = new URL( protocol + "://" + host + ":" + port); // Create the proxy object db = new UserDatabaseProxy(servletBase);With the original RMI code replaced with this new proxy object code, you're basically done with your applet changesno other modifications are necessary, since the function call syntax on db the proxy object is the same as it was for db the RMI object. You cheated here a little by glossing over exception handling, since your remote method calls throw RemoteException while your proxy object methods may throw a variety of exceptions (MalformedURLException, ClassCastException, etc.), but you get the idea.
That's ityou're done. Your client applet can now use HTTP instead of RMI to communicate with the database access object on the server end, and you didn't even have to change a single line of your existing server code (and only a half dozen lines in the applet). With just these few changes you've made your applet more compatible, your server more flexible, and your communication much more reliable and Internet-friendly. Beyond the BasicsIn modifying your sample application above you designed your servlets to mimic what your RMI object doesnamely, they take some number of parameters and return a single object of a specific type (or none). But unlike RMI method calls, your servlets can be extremely flexible in the way that they handle and return data. The following examples illustrate a few of the options available for applet/servlet/RMI applications.Receiving Multiple Objects From One CallIn your above changes you created a servlet that, like your RMI object, responds to an applet request by returning a Hashtable that may contain a large number of objects. That works, but as with the original RMI method call, the client will be left waiting for the entire Hashtable to be returned. This prevents your applet from displaying a progress bar or stopping the operation after only a subset of values have been retrieved, highly desirable features if a large number of entries are involved. (Of course, you can retrieve a list of objects one-at-a-time by making a separate server call for each one, but doing so on thousands of objects is impractical.)
Instead of waiting for your servlet to return an
public void doPost(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
ObjectInputStream in =
new ObjectInputStream(
req.getInputStream() );
ObjectOutputStream out =
new ObjectOutputStream(
resp.getOutputStream() );
try {
Hashtable database =
db.listUsers();
out.writeObject(database);
} catch (
Exception e) { e.printStackTrace(
); }
// Close the I/O streams
in.close();
out.close();
}
Here's another version of the above servlet, but this one informs the client how many entries there are in the return value, and then sends the Hashtable one key and value at a time:
public void doPost(
HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
ObjectInputStream in =
new ObjectInputStream(
req.getInputStream() );
ObjectOutputStream out =
new ObjectOutputStream(
resp.getOutputStream(
) );
try {
Hashtable database =
db.listUsers();
// Tell the client how many
//entries are coming
int num = database.size();
Integer numInt = new Integer(
num);
out.writeObject(numInt);
// Write each key and
//value of
//the Hashtable
Enumeration keys =
database.keys();
while (
keys.hasMoreElements(
) )
{
String nextKey =
keys.nextElement();
UserData nextData = (
UserData)database.get(
nextKey);
out.writeObject(nextKey);
out.writeObject(nextData);
}
} catch (
Exception e) { e.printStackTrace(
); }
// Close the I/O streams
in.close();
out.close();
}
In this new version of the servlet you're sending exactly the same Hashtable data as the old version, but you're now letting the client know how much data is coming before you start sending it. This gives the client a lot more flexibility in dealing with this data.
Obviously, if you change your servlet like this you need to change your proxy object to deal these return values. Here's the original code from your sample application's proxy object:
public Hashtable listUsers(
) throws Exception
{
servlet = new URL(webBase,
"servlet/UserListServlet");
Serializable objs[] = {};
in = ServletWriter.postObjects(
servlet, objs);
Hashtable database = (
Hashtable)in.readObject();
in.close();
return database;
}
Here's another version of the same function that retrieves data from the new servlet, and also displays its progress in the applet's UI:
public Hashtable listUsers(
java.awt.Label label) throws Exception
{
servlet = new URL(
webBase, "servlet/UserListServlet");
Serializable objs[] = {};
ObjectInputStream in =
ServletWriter.postObjects(
servlet, objs);
// Initialize the return value
Hashtable database = new Hashtable(
);
// Get the number of data
//values coming
Integer numInt = (
Integer)in.readObject();
int num = numInt.intValue();
// Read each key/value pair and
//add to the Hashtable
for (int x = 1; x <= num; x++)
{
label.setText(
"Getting data (
" + x + "/" + num + ")...");
String nextKey = (
String)in.readObject();
UserData nextData = (
UserData) in.readObject();
database.put(
nextKey, nextData);
}
in.close();
return database;
}
When the applet executes this version of listUsers(), the given label displays the total number of entries and the current number being read (for example, "Getting data (7/26)...").
Combining ServletsAnother change you can make to the sample application is to consolidate all of your servlets into a single servlet, with all of the RMI methods calls contained in a singledoPost() method. This complicates the servlet because it will need to switch between several code blocks based on the operation to perform, but it simplifies deployment by eliminating the need for multiple servlets.
Here's a single servlet that implements all of the database method calls that your five servlets, using an integer code sent by the applet to determine the appropriate action:
public class UniversalServlet extends DatabaseServlet
{
static final int LIST = 0;
static final int GET = 1;
static final int CREATE = 2;
static final int EDIT = 3;
static final int DELETE = 4;
public void doPost(
HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
// Open the I/O streams
ObjectInputStream in =
new ObjectInputStream(
req.getInputStream() );
ObjectOutputStream out =
new ObjectOutputStream(
resp.getOutputStream(
) );
try {
// Find out which operation to perform
Integer codeInt = (
Integer)in.readObject();
int code =
codeInt.intValue();
if (code == LIST) {
Hashtable database =
db.listUsers();
out.writeObject(database);
}
else if (code == GET) {
String id = (
String)in.readObject();
UserData data =
db.getUser(id);
out.writeObject(data);
}
else if (
code == CREATE) {
UserData data = (
UserData)in.readObject();
String id =
db.createUser(
data);
out.writeObject(id);
}
else if (code == EDIT) {
String id = (
String)in.readObject();
UserData data = (
UserData)in.readObject();
db.editUser(id, data);
}
else if (code == DELETE) {
String id = (
String)in.readObject();
db.deleteUser(id);
}
} catch (
Exception e) { e.printStackTrace(
); }
// Close the I/O streams
in.close();
out.close();
}
}
Notice in this new servlet code that parameters for each operation are read only after the integer code is read and you determine which code block to execute. Unlike your previous servlets, you can't read all parameters from the input stream at the beginning of this doPost() because you don't know the number and type of parameters to read until after you know which operation to execute.
To use this servlet you need to change all methods of your proxy object accordingly, using an integer code to specify the operation to perform. For example, here's a version of the proxy object's createUser() method that uses the above UniversalServlet:
public String createUser(
UserData data) throws Exception
{
URL servlet = new URL(
webBase,
"servlet/UniversalServlet");
Integer code =
new Integer(2); //CREATE
Serializable objs[] =
{ code, data };
ObjectInputStream in =
ServletWriter.postObjects(
servlet, objs);
String id = (
String)in.readObject();
in.close();
return id;
}
The only change you've made here is to create an Integer that corresponds to the operation being performed (in this case, creating a database entry) and add this Integer to the array of parameters posted to the servlet.
Handling Multiple Object TypesYou can also construct your applet to accommodate different object types in response to the same servlet call. For example, if while retrieving database information your servlet encounters aRemoteException, you may want your applet to receive the exception. To do so, you can write the RemoteException object to the servlet's ObjectOutputStream instead of the return value you were expecting. Although this would cause a ClassCastException to occur in the proxy object when it attempted to read the return value, you can use instanceof to determine whether the servlet output the return value or an exception, as in this example:
public UserData getUser(
String userid) throws Exception
{
URL servlet = new URL(webBase,
"servlet/UserGetServlet");
Serializable objs[] = { userid };
objectInputStream in =
ServletWriter.postObjects(
servlet, objs);
// Read the value as an Object
Object obj = in.readObject();
// Is it an Exception?
if (obj instanceof Exception) {
throw (Exception)obj;
}
else {
UserData data = (
UserData)obj;
in.close();
return data;
}
}
Here you're reading in the return value from the servlet as type Object and then using instanceof to determine whether it's an Exception. If it is, you cast it as an Exception and throw it to let the applet deal with it as necessary; otherwise, you assume the return value is a UserData object, cast it as such, and return it to the applet.
ConclusionAs you can see, your options in working with applet/servlet communication are far greater than when using RMI in your applet; the examples shown here are just a start. While inserting another layer in your application may take a little more work, the payoff is an applet that is more compatible, client/server communication that is more flexible, and an RMI server application that is more reliable.
About the AuthorScott McPherson Scott McPherson is the founder and President of Maczieg Software, a Silicon Valley-based Java software company. Scott and his company recently launched MochaMail Online the Internet's first Java-based free email service. Reader FeedbackTell us what you think of this article.
|