| |
|
|
Java Tip 39: The trick to using a basic Java 1.1 network and file class loader
Having trouble with that class loader? Here's how to use one in Java 1.1 that works either over the 'Net or from a local file
Summary
This tip shows you how to use a Java 1.1 class loader that works either
over the Internet or from a local file. It also illustrates how to use
the class loader correctly to "bootstrap" your classes. It's based on a
previous JavaWorld article by the popular columnist, Chuck
McManis. (1,850 words)
By Jack Harich
|
Java Tips
|
For a comprehensive list of Java Tips published in
JavaWorld, see the
Java Tips Index
Do you have a tip that would benefit JavaWorld's
readers? We would like to pass it on! Submit your tip to
javatips@javaworld.com.
|
One of the most frequent advanced Java questions I encounter is, "I'm
having trouble loading classes over the Internet." In fact, in my own
experience, when I finally had to deploy an application on an intranet,
my class loaders failed. My attempts to take a shortcut and use the
built-in RMIClassLoader succeeded with Java 1.0 applets
but failed with Java 1.1 programs.
For his "Java In Depth" column in the October '96 issue of
JavaWorld, Chuck McManis discussed in "The basics of Java
class loaders." Based on that column, this Java Tip demonstrates
how to successfully use the Java 1.1 class loader and goes a lot further.
Java class loader requirements
Let's first examine our requirements for a class loader. We'd like one
that:
- can load classes from any legitimate URL pointing to a class file
- can also load classes from any local class file; this is useful for
testing or simulating an Internet server on a client.
- can optionally translate periods in the class name to a preferred
character, such as "_"; this is useful for placing classes from
different packages/directories into the same directory
- follows the rules that all class loaders must obey
High-level design
The most flexible approach to the above requirements is these three
classes:
public abstract class MultiClassLoader extends ClassLoader
public class URLClassLoader extends MultiClassLoader
public class FileClassLoader extends MultiClassLoader
This allows the type of class loader to be determined at runtime and the
rest of the application is unaffected.
Unit test
Hmmmm, fulfilling the above requirements doesn't sound too hard. Let's
start with a "Use Case," namely a code example of how we'll use
MultiClassLoader. This is best done by designing our test
class, called TestLoader. Now, let's write the TestLoader
unit test class. This shows how to set the class loader type at
runtime, and then uses it in a variety of ways to illustrate how to use
a class loader effectively. Here is the source code for TestLoader.java.
Let's examine the heart of the test code:
// Init the preferred class loader
MultiClassLoader loader = null;
if (args.length == 0) {
String root = "http://www.mindspring.com/~happyjac/";
loader = new URLClassLoader(root);
} else {
loader = new FileClassLoader("store\\");
}
loader.setClassNameReplacementChar('_');
// A series of tests:
Class testClass = null;
try {
testClass = loader.loadClass("Hello");
} catch(Exception ex) {
print("Load failed");
ex.printStackTrace();
return;
}
print("Loaded class " + testClass.getName() );
try {
Runnable hello = (Runnable)testClass.newInstance();
hello.run();
} catch(Exception ex) {
print("Failed to instantiate");
ex.printStackTrace();
}
Good tests are understandable and short. The test first creates an
instance of class loader called "loader". Then it uses an optional
feature to translate periods to "_".
Next we decide whether to load from the Internet or from a local file,
depending on whether any command-line argument was entered. Note that
you will need to set the root or filePrefix to whatever
source URL you're using as a base. We have subclassed
MultiClassLoader, providing a clean way to provide
different specific class sources.
Then we attempt to load the Hello class, and we abort if
the load fails. If an exception is thrown, it will be displayed on the
command line. This is done with only one line of code, which
illustrates the architectural strength of Java:
testClass = loader.loadClass("Hello");
So far, so good. We've loaded the class, called testClass.
Now we need an instance of testClass, easily done with the
newInstance() method. But we also need to cast the
Object returned by newInstance() to a class
known by the class loader that loaded TestLoader. This is
usually the primordial class loader. One way is to provide the desired
interface locally, such as EntryPoint.class. But this
violates the strategy of thin client, so instead we cast Object to
Runnable, which is provided by the Java core API. This is all done in
one of the most powerful lines of Java code you will encounter:
Runnable hello = (Runnable)testClass.newInstance();
The Runnable interface has only one method, run(), so we
next do:
hello.run();
And poof! -- we're done. That's it: three important lines of code, and the
rest is done by Java and our MultiClassLoader. This three-line technique is known as "bootstrapping the client." Relax and rest
awhile, three lines of code is hard work.
A common mistake is to cast the object to something like
EntryPoint, and when it works, to assume that all's well.
Actually, the most likely reason it worked is because the primordial
or another class loader found your local copy of
EntryPoint.class using CLASSPATH when
findSystemClass() was called; you probably thought it was
loaded from elsewhere.
When Hello starts running, it runs an additional test to
prove that MultiClassLoader is used to load "foreign"
classes referenced by Hello, and that casting to foreign
interfaces works. The test classes are Hello.java, Worker.java,
and Person.java.
Examining MultiClassLoader
Let's examine the end result of loading Hello.class with
our class loader. We now have Hello running. Any classes
referenced by Hello will be loaded by the class loader
that loaded Hello -- namely MultiClassLoader.
The source code is MultiClassLoader.java.
As we can see from our previous test, the key method is:
public Class loadClass(String className) throws ClassNotFoundException {
return (loadClass(className, true));
}
The method loadClass(String className) is a convenience
method that calls another method that does the real work. This other
method, loadClass(className, true), is the abstract method
in java.lang.ClassLoader that all subclasses must
implement. Here's the code:
public synchronized Class loadClass(String className,
boolean resolveIt) throws ClassNotFoundException {
Class result;
byte[] classBytes;
monitor(">> MultiClassLoader.loadClass(" + className + ", " + resolveIt + ")");
//----- Check our local cache of classes
result = (Class)classes.get(className);
if (result != null) {
monitor(">> returning cached result.");
return result;
}
//----- Check with the primordial class loader
try {
result = super.findSystemClass(className);
monitor(">> returning system class (in CLASSPATH).");
return result;
} catch (ClassNotFoundException e) {
monitor(">> Not a system class.");
}
//----- Try to load it from preferred source
// Note loadClassBytes() is an abstract method
classBytes = loadClassBytes(className);
if (classBytes == null) {
throw new ClassNotFoundException();
}
//----- Define it (parse the class file)
result = defineClass(classBytes, 0, classBytes.length);
if (result == null) {
throw new ClassFormatError();
}
//----- Resolve if necessary
if (resolveIt) resolveClass(result);
// Done
classes.put(className, result);
monitor(">> Returning newly loaded class.");
return result;
}
The method is not very different from Chuck's original article.
Subclassing ClassLoader is much simpler that writing the
entire class loader. Note the use of monitor() for
debugging. The comments in the code listing are straightforward, so I
won't give you a detailed explanation here. If you'd like one, though,
read Chuck's original article.
The most important thing loadClass() does is to perform a
series of steps in the correct order, to satisfy the design of class
loaders. If these steps are out of order, one is missing, or one is
improperly implemented, you will witness abnormal behavior or security
breaks. Satisfying these requirements cannot be over-emphasized.
Notice the "//----- Try to load it from preferred source"
block. This is the heart of the entire class, and it illustrates the
beauty of the class loader concept: All a class loader does is get an
array of bytes from a source somewhere and feed these bytes to Java
appropriately. Such sweet simplicity! In our case, we are calling an
abstract method, implemented by subclasses URLClassLoader
or FileClassLoader. Note how you could easily subclass to
handle more sources, such as a relational database.
The subclasses source code is URLClassLoader.java and FileClassLoader.java.
Let's look at URLClassLoder's key method:
protected byte[] loadClassBytes(String className) {
className = formatClassName(className);
try {
URL url = new URL(urlString + className);
URLConnection connection = url.openConnection();
if (sourceMonitorOn) {
print("Loading from URL: " + connection.getURL() );
}
monitor("Content type is: " + connection.getContentType());
InputStream inputStream = connection.getInputStream();
int length = connection.getContentLength();
monitor("InputStream length = " + length); // Failure if -1
byte[] data = new byte[length];
inputStream.read(data); // Actual byte transfer
inputStream.close();
return data;
} catch(Exception ex) {
print("### URLClassLoader.loadClassBytes() - Exception:");
ex.printStackTrace();
return null;
}
}
Creating loadClassBytes() is not at all difficult, thanks
to Java's many handy classes. All the real work is done in one line of
code: inputStream.read(data);
A final word of caution: Each subclass of MultiClassLoader
should be used for only one source. Do not change a class
loader's source after the class loader is instantiated. This will
prevent disasters like a class with the same name but a different
source being loaded from the wrong source -- which could happen if the
source was changed dynamically. It will also make security breaks more
difficult. Avoid the temptation to reset the source. Instead, design
your subclasses to set the preferred source once and only once, so help
you Java!
How to run your very own test
On http://www.mindspring.com/~happyjac/, we have the following files:
Hello.class
test_store_Person.class
test_store_Worker.class
Warning: These are files that run as an application --
not as an applet. They are trusted and are merely compiled classes
from the source files mentioned above.
Download MultiClassLoader, URLClassLoader,
FileClassLoader, and TestLoader (all .java
files). Put them in the same directory, named "test," and compile
TestLoader, which automatically also compiles the other
classes. Open your Internet connection, such as by dial-up. Then run
TestLoader via the usual java TestLoader. You
should see:
Loading from URL: http://www.mindspring.com/~happyjac/Hello.class
Loaded class test.store.Hello
Hello class instantiated
Hello.run() called
Loading from URL: http://www.mindspring.com/~happyjac/test_store_Worker.class
Loading from URL: http://www.mindspring.com/~happyjac/test_store_Person.class
Worker class instantiated
Worker.startWorking() called
Worker class instantiated
Person first name is FirstName
Tested casting Worker to Person
Test complete
To test the ability to load classes from a file, add a subdirectory to
the "test" directory named "store." Download the files
Hello.java, Person.java, and
Worker.java. Compile them. Rename
Person.class to test_store_Person.class and
Worker.class to test_store_Worker.class. Then
on the "test" directory, run java TestLoader x without your
dial-up open. You should see output similar to the first test except
that it's loaded locally.
That's it. Have fun with your very own class loader. It gives you the
power (with permission) to load a class from anywhere on the network.
This means that if, as Sun claims, "the network is the computer," then
you are now the network's absolute master. Once you understand the
power of custom class loaders, they are a viable alternative to
browsers for thin client. Thanks to Chuck McManis for the concepts in
his JavaWorld column on class loaders!
About the author
Jack Harich, aka "Happy Jack," is a fun-loving Renaissance man who
switched to software after a career as a sculptor came to a quick end
due to a neck injury. He's currently a consultant in Atlanta (the
Silicon Cotton Field of the South) and is very active with the Atlanta
Java User's Group, its Java As A Second Language SIG, and the Atlanta
Java Consortium.
|