USING REMOTE METHOD
INVOCATION TO ACCESS LEGACY
DATABASES
Remote Method Invocation (RMI) is a way to communicate between
two Java(tm) programming language applications that are remote
from each other in a network.
RMI differs from lower-level communication mechanisms like
sockets in a couple of important areas. One is that you can use
RMI to do remote method calls in a natural way. That is, you can
obtain a reference to an object on a remote server and then
make method calls on the object. You can also do remote method
calls with sockets, but you have to set up your own arrangements
for argument passing, return value passing, and so on; this
approach isn't integrated into the Java programming language
very well. RMI takes care of these details for you.
Another difference is that RMI requires Java support. If you have
a server and a client, and they communicate using RMI, then
both need to be implemented as Java programs. This is because of
the underlying Java facilities used to implement RMI, such as
serialization. It's also because of the Java language semantics
implied by RMI, such as interfaces.
But the requirement that you use Java on both ends doesn't prevent
you from using RMI with other languages. And that's one of the
things that this tip illustrates. The tip presents a client/server
RMI-based application that accesses a legacy database via C and
C++ functions. Here "legacy" means an older application, one that
isn't necessarily written using Java programming or that is
network aware.
So this is a fairly complex application, one where you have a:
o Database
o C function that accesses a database
o C++ wrapper function that implements a Java native method,
and calls a C function
o Server that creates a remote object and registers it
with an RMI registry
o Client that looks up the remote object reference via the
registry. The client makes a method call on the object to access
the database through the native method wrapper and the C
function.
The RMI registry, server, and client all run on the same machine,
but the tip fully illustrates areas like security and codebases.
One important thing to note about this tip...
although the tip demonstrates a working RMI application, it is not a
tutorial on RMI. The last section of the tip points to documentation
that you read to learn more about RMI.
How This Tip is Organized
Because the application is fairly complex, this tip is longer
than the typical JDC Tech Tip. To make it easier to follow, the tip
is organized in three parts:
Part 1. Developing the Application. This part defines the pieces
that comprise the application. The tip provides source code for
each of these pieces.
Part 2. Putting it all Together. This part describes how to put
all the pieces together into a working application.
Part 3. Running the application. This part describes the actions
you need to take to run the application.
Part 1. Developing the Application
Developing an RMI application involves steps such as defining an
interface for remote objects and implementing the interface. This
part of the tip describes how to develop this particular RMI
application, that is, one that accesses a remote database through
C and C++ functions.
1. Define an Interface For Remote Objects
The first step in developing an RMI application is to define
an interface that remotely-called objects must implement.
In this application, the interface is called Search, and it
is in a package called rmitest:
// Search.java
package rmitest;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Search extends Remote {
public String findEntry(String w) throws RemoteException;
}
Search declares a single method findEntry, that is used to
look up an entry in a database (such as a phone or address
list), and return the entry that it found. In other words,
the server application registers (with an RMI registry) an
instance of a class that implements the Search interface.
A client can then consult the registry to obtain a reference
to the instance's stub. The client can call methods via the
stub to effect database lookups. A further discussion of stubs
is found later in the tip.
An interface that describes remote class functionality must
extend the java.rmi.Remote interface. Also, methods must
declare that they throw java.rmi.RemoteException.
RemoteException is used to handle the special types of failures
that can occur in RMI, such as a network failure.
2. Define an RMI Remote Object That Implements the Interface
After the interface for a remote class has been defined, you need
to implement the interface, that is, define a remote class. In
this example, the class is called SearchImpl:
// SearchImpl.java
package rmitest;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
public class SearchImpl extends UnicastRemoteObject
implements Search {
// load C/C++ shared library
static {
System.loadLibrary("rmilib");
}
// constructor
public SearchImpl() throws RemoteException {
super();
}
// public remotely-callable method that finds an entry
public String findEntry(String w) {
return findEntry0(w);
}
// native C++ method to actually look up an entry
private native static String findEntry0(String w);
}
All this class does is load a shared library, that contains the
C and C++ functions used to search the database. Calls to
findEntry are simply passed through to a native method
findEntry0.
SearchImpl extends a class java.rmi.server.UnicastRemoteObject.
This class provides some of the basic RMI functionality, such as
support for remote object references.
3. Define a C Function That Searches a Database
The C part of this application is a function that accesses some
type of legacy database. For this application, a function called
"func" searches a text file for a string. It then returns the
first line of the file that contains the string. You can use
a function like this to look up names in a phone directory. The
function consults a text file "data.txt", which is the database.
/* func.c */
#include
#include
#include
/* database */
char* db = "data.txt";
/* process input -> output */
void func(char* in, char* out) {
char inbuf[256];
int found = 0;
FILE* fp;
/* input string is empty */
if (in == NULL || !strcmp(in, "")) {
strcpy(out, "*** not found ***");
return;
}
/* open database */
fp = fopen(db, "r");
if (fp == NULL) {
strcpy(out, "*** database open error ***");
return;
}
/* search database and return first matching line */
while (fgets(inbuf, sizeof inbuf, fp) != NULL) {
if (strstr(inbuf, in) != NULL) {
found = 1;
break;
}
}
fclose(fp);
/* found an entry, clip off newline */
if (found) {
size_t len = strlen(inbuf);
if (len >= 1 && inbuf[len - 1] == '\n')
inbuf[len - 1] = 0;
strcpy(out, inbuf);
}
else {
strcpy(out, "*** not found ***");
}
}
4. Define a C++ Wrapper / Java Native Method For the C Function
After you define the C function, you define a C++ wrapper
function for the C function. You might ask "why not use the
C function directly as a native method callable from Java?"
The reason for the C++ wrapper is that native methods
implemented in C/C++ require a particular name or signature.
If you're accessing legacy functions and data, you may not be
able to change the name of existing functions. So another
function is defined as a wrapper around the legacy function.
This function has a name:
Java_rmitest_SearchImpl_findEntry0
that corresponds to a Java native method:
rmitest.SearchImpl.findEntry0
How do you know what name to give this wrapper function? You can
use the "javah" tool to generate a declaration for the wrapper,
by saying:
javah -jni -o rmilib.cpp rmitest.SearchImpl
This command generates a Java(tm) Native Interface (JNI)
declaration for a native method. You don't need to use the
javah tool in this application because the declaration of the
native method, and its implementation, are provided here:
// rmilib.cpp
#include
#include
#include
// C function that searches database
extern "C" void func(char*, char*);
// declaration for native method rmitest.SearchImpl.findEntry0()
extern "C" {
JNIEXPORT jstring JNICALL Java_rmitest_SearchImpl_findEntry0
(JNIEnv *env, jclass, jstring str) {
char inbuf[256];
char outbuf[256];
// get the input string
const char* s = env->GetStringUTFChars(str, NULL);
if (s == NULL)
return NULL;
// copy it out to a char buffer
strcpy(inbuf, s);
env->ReleaseStringUTFChars(str, s);
// call C function
func(inbuf, outbuf);
// format output for return
return env->NewStringUTF(outbuf);
}
}
This wrapper takes an input string and converts it to a
C-style string (a sequence of bytes terminated by a null byte).
The wrapper then calls the C legacy function and formats the
string for return to the calling Java program.
After the two C/C++ functions are compiled, they are grouped
in a shared library. This makes the functions callable from
a Java application.
5. Define a Server Program
So far you've defined a remote interface and class, and a couple
of C and C++ functions that issue remote object calls. But how
do you make a remote object "do" something?
The first step is to register a remote object instance. This
means you create a remote object in a server and then call
java.rmi.Naming.rebind to associate that object with a name.
This process uses a registry program that can remember the
name-object association. If an object is registered, a client
program can call java.rmi.Naming.lookup with the associated name;
lookup will then return the registered object. More precisely,
lookup returns a stub to the remote object. Stubs are described
in the section on clients later in the tip.
The Java(tm) Development Kit has a program called "rmiregistry"
that you use as a registry. You start this program before
starting the RMI server and client. The rmiregistry program
remembers name-object associations specified by the RMI server
program. This allows an RMI client program to look up an
association by name, and obtain a stub reference to a remote
object. Here is the server program for the application.
// Server.java
package rmitest;
import java.rmi.Naming;
public class Server {
public static void main(String args[]) {
// install RMI security manager
System.setSecurityManager(new SecurityManager());
// create a remote object and register it
try {
SearchImpl si = new SearchImpl();
Naming.rebind("searchobj", si);
}
catch (Exception e) {
System.err.println(e);
}
}
}
Notice that the client program creates a remote object and
registers it using the name "searchobj". It also installs
a security manager. (The server program will need to do this
too.) Without a security manager, the RMI class loader will
not download classes from remote locations. The security
manager protects against malicious operations by loaded
classes.
6. Define a Client Program That Calls the Server
The final piece you need to define is a client program. This
program takes an input string that you specify and uses it
to look up information in the database. Specifically, the
client issues RMI calls to the findEntry method of the remote
object that the server program registered.
Recall that the server program registers a remote object using
the name "searchobj". The client program uses Naming.lookup to
look up the remote object; it also specifies the host name
where the server is running:
file://localhost/searchobj
The name "localhost" is a special name for the local machine
with IP address 127.0.0.1 (another name for this is the
"loopback" address). In this example, both the client and the
server are running on the same machine. If you run the server
on a different host, you would replace "localhost" with that
host name.
Here is the client program for the application.
// Client.java
package rmitest;
import java.rmi.Naming;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Client {
// do call on remote object
public static String lookup(String str) {
String t = null;
try {
String host = "file://localhost/searchobj";
Search s = (Search)Naming.lookup(host);
t = s.findEntry(str);
}
catch (Exception e) {
System.err.println(e);
}
return t;
}
public static void main(String args[]) {
JFrame frame = new JFrame("RMI Client");
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
// set RMI security manager
System.setSecurityManager(new SecurityManager());
// set up input and output areas
final JTextField field = new JTextField(25);
field.requestFocus();
final JLabel label = new JLabel(" ");
// process input
field.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
label.setText(lookup(field.getText()));
field.requestFocus();
}
});
// set up panels and so forth
JPanel panel1 = new JPanel();
JPanel panel2 = new JPanel();
panel1.add(field);
panel2.add(label);
frame.getContentPane().add("North", panel1);
frame.getContentPane().add("South", panel2);
frame.pack();
frame.setVisible(true);
}
}
The heart of this code is the three lines:
String host = "file://localhost/searchobj";
Search s = (Search)Naming.lookup(host);
t = s.findEntry(str);
The first two of these look up a remote object by name
("searchobj"). The last line does the database lookup using
the remote object's stub.
You might wonder whether the client "really" is operating on
the remote object. In other words, does the client have local
to itself the actual remote object? The answer is "no", and
this gets at the heart of what's going on inside of RMI.
In Part 2 of this tip, "Putting it All Together," you will
run a tool called the RMI compiler (rmic). This tool generates
a "stub" for the SearchImpl class. The stub exists on the
server, but can be dynamically downloaded to the client. So
when you run the client, it interacts with the remote object
via the stub. It's the stub (not the object) that is responsible
for formatting and transmitting method arguments ("marshalling")
to the RMI system on the server. A "skeleton" class on the
server side "unmarshals" this information and makes the actual
call on the remote object. The process is reversed to transmit
return values back to the client.
In this example, the client side has available locally the class
files Client.class and Search.class. The stub class
SearchImpl_Stub.class is downloaded on demand from the server.
The actual remote class SearchImpl.class is not downloaded to the
client. So the remote object stays on the server, and the client
interacts with it via the stub class instance.
Another thing to keep in mind is the concept of a "codebase."
This is where stubs are stored. In this application, a codebase
is specified when you run the server. The specification looks
like this:
-Djava.rmi.server.codebase=http://localhost:2001/
When you register a remote object using Naming.rebind, a
codebase for the object is recorded. When the object's stub
needs to be downloaded, it can be found using the codebase.
In this application, a simple HTTP server is used to serve up
stub .class files from the codebase. This server uses port 2001
on localhost.
Codebases have some relation to CLASSPATH settings. When
rmiregistry tries to find a file, it looks at the CLASSPATH
first and then the codebase. This particular application is
configured so that the codebase settings must be observed for
the application to work.
Notice that like the client program, the server program installs
a security manager to protect against malicious operations by
loaded classes.
Part 2. Putting It All Together
Here's how to take the various pieces of the application that you
defined and assemble them into a working application:
1. Create a base directory. This tip uses the name
base. The directory might actually be something like:
/usr/jones/rmibase
on UNIX, or:
c:\rmibase
on Windows. If you're using Windows, replace all / with \, but
leave http:// paths alone.
2. Create a directory structure under base:
base/client
base/client/rmitest
base/server
base/server/rmitest
base/src
base/examples
base/examples/classServer
3. This tip assumes that JDK 1.2.x is installed in:
/jdkbase
4. Copy all of the above source files to base/src.
5. Change directories to base/src, and compile the C legacy
function by saying:
cc -c func.c
This compiles func.c to an object file (func.obj for Win32, or
func.o for Solaris).
If you're using a Windows C/C++ compiler like Borland C++, you
might say:
bcc32 -c -P- func.c
func.c is a C function, not a C++ one, so use appropriate
compiler options to specify C compilation.
6. Compile the C++ native function by saying:
c++ -c -I/jdkbase/include -I/jdkbase/include/win32 rmilib.cpp
where "c++" is your local C++ compiler. If you're using Solaris,
replace "win32" with "solaris". This step also produces an object
file (rmilib.obj or rmilib.o).
7. Create a shared library by saying:
bcc32 -tWD rmilib.obj func.obj
With Solaris you say:
cc -G -o librmilib.so rmilib.o func.o
In other words, you combine the two object files into a shared
library.
You should now have a library rmilib.dll (Win32) or
librmilib.so (Solaris). Move this library to base/server. Note
that there's a platform-dependent mapping between the library
name specified to System.loadLibrary and an actual shared
library name on your system. For example, loading a library
named "abc" implies a name "abc.dll" for Win32, and "libabc.so"
for Solaris.
This application assumes that the current directory (".") is
in the search path for loading shared libraries into a Java
program. If you have trouble with this area, you might wish to
change your "java.library.path" setting. This setting is forced
in the server invocation below.
8. In base/server create a small text file called data.txt, with
lines like this:
Jane Jones 457-9231
Tom Garcia 143-5876
Bill Smith 456-8918
Separate the fields of each line with spaces. This is the
legacy database that will be searched by the application.
9. Change directories to base/src, and say:
javac Search.java SearchImpl.java Server.java Client.java
10. Copy:
Search.class SearchImpl.class Server.class
to base/server/rmitest.
11. Copy:
Search.class Client.class Client$1.class Client$2.class
to base/client/rmitest.
12. Change directories to base/server, and say:
rmic -classpath . -d . rmitest.SearchImpl
This step generates the SearchImpl_Skel.class and
SearchImpl_Stub.class files.
Part 3. Running the Application
Here's what actions you need to take to run the application.
1. Create a desktop window and say:
rmiregistry
Do this from a directory positioned such that server/rmitest
is not reachable via your CLASSPATH. In other words, start
rmiregistry from somewhere unrelated to this application's
source and .class files. This is done to avoid confusing
CLASSPATH and codebase locations when searching for stub files.
The rmiregistry program doesn't display or print anything. It
just waits for registry requests.
2. Download the class server from:
ftp://ftp.javasoft.com/pub/jdk1.1/rmi/class-server.zip
This is a small download (5K). Unzip the files into the
directory base/examples/classServer.
Change directories to base/examples/classServer and say:
javac ClassFileServer.java ClassServer.java
3. Run the class server in a new window by changing directories to
base and saying:
java examples.classServer.ClassFileServer 2001 base/server
2001 is the class server's port, and base/server is where the
server looks for .class files. So the server will look for
SearchImpl_Stub.class with the pathname
base/server/rmitest/SearchImpl_Stub.class.
4. Run the RMI server in a window by changing directories to
base/server and saying:
java \
-Djava.library.path="." \
-Djava.security.policy=base/server/java.policy.server \
-Djava.rmi.server.codebase=http://localhost:2001/ \
rmitest.Server
java.policy.server is a policy file in base/server. It's a text
file that should contain these lines:
grant {
permission java.lang.RuntimePermission "loadLibrary.*";
permission java.util.PropertyPermission "user.dir", "read";
permission java.net.SocketPermission
"*:1024-65535", "connect,accept";
};
The policy file specifies permissions used by the security
manager installed in the server.
5. Run the client in a window by changing directories to
base/client and saying:
java \
-Djava.security.policy=base/client/java.policy.client \
rmitest.Client
java.policy.client is a policy file in base/client. It's a text
file that should contain these lines:
grant {
permission java.net.SocketPermission
"*:1024-65535", "connect,accept";
};
6. Enter a string into the input area of the client GUI. The
application will use the string to search the legacy database.
It will return and display the first line in the database that
contains a match.
Further Information
There are a number of RMI papers available online. These four
provide basic information about RMI, describe codebases,
and answer common questions:
http://java.sun.com/products/jdk/rmi/
http://java.sun.com/products/jdk/1.2/docs/guide/rmi/getstart.doc.html
http://java.sun.com/products/jdk/1.2/docs/guide/rmi/faq.html
http://java.sun.com/products/jdk/1.2/docs/guide/rmi/codebase.html
This paper describes how to use the Java Native Interface to
combine Java and C/C++ code:
http://java.sun.com/docs/books/tutorial/native1.1/
|