100 lines or less: Test Re-factoring with Multiple Class loaders
Table of Contents
Recently I ran into a problem. I had some serious re-factoring to do. When I use the term re-factor I try to do so carefully. In my personal dictionary re-factor is only appropriate when you are making changes to code that does not modify the signatures or behaviors of it's public, documented API. In this instance we were deprecating a database that had some design flaws. My team and I had to make various operations of our model layer run against the new database, but we had to guarantee the results of the application did not change.
And so it hit me, in Java, the discovery of dependencies is managed in Java, at run time. This mechanism (as you probably know) is called a ClassLoader. I figured writing a small test loading application that would utilize multiple ClassLoader instances to define the class byte code for the same method(s), and tests might do the job. As I looked further into this, I discovered it's not really possible to compare objects owned by different class loaders. In theory, would be, however most equals method implementations begin as such:
boolean equals (Object object) {
if (object instanceof ThisClass) return false;
}
Unfortunately verifying the objects being compared are both instances of the same class is a completely reasonable thing to do. I realized I would need something trickier. Quickly, let's run over how class loaders work.
Dependency discovery
As a Java application is running and a new class name is discovered, a discovery search takes place to see if the class has previously been defined. Here is a quick break down of how this discovery process works. The optimizations of a modern JVM are such that the process doesn't happen this way verbatim, although this explanation is reasonably accurate for most intents and purposes.
Figure 1.0: Class loader discovery
First, the system class loader would be checked to see if it owns a class of such a name, and so on down the chain until the class loader which owns the currently running class is checked. Assuming none of the class loaders already have defined a class with such a name, the class loader which loaded the currently executing class is queried for the class through the loadClass method. The class loader has at this point an opportunity to define the class itself, or throw a ClassNotFoundException, or another exception. If any run time exceptions are thrown, they're reported to the user and the application should fail fast.
Building a Custom Loader
Building a custom class loader can be the solution to the behavior of equals. An object derived from a class is inherently not equal to another object derived from an identical class of the same name if it's loaded by a different class loader. Therefor, we ensure the classes we are using to represent the values returned by our tests are loaded using a single class loader. To do so, we will intercept all dependency requests by loading our class with a custom class loader. This class loader can arbitrarily delegate the loading of dependencies to the system class loader, based on arbitrary criteria.
Figure 2.0: Our Custom Class loader
Our class loader will work as follows. Any classes in an arbitrary set of class names will be passed on to the system class loader. In this case we're arbitrarily bypassing any other parent class loader(s) we may have. There is little reason for this other than convenience and a better guarantee that our values will be comparable. We will do this by defining a loadClass method in a subclass of ClassLoader.
import java.io.*;
import java.util.*;
import java.util.jar.*;
class UlteriorClassLoader extends ClassLoader {
We need to look up our classes somewhere, and because we're ultimately doing re-factoring testing it seems to make sense to make this some place a jar file. So we'll make our class loader open and search a jar for our classes. This allows us to simply snag the jar of an older build of our application to compare against.
private JarFile _jar;
UlteriorClassLoader (File file)
throws IOException {
_jar = new JarFile(file);
}
Now, many class loader implementations (particularly expensive ones) will attempt to call loadClass() on the parent class loader first, loading any classes at the highest level possible. In order to get the behavior of being able to run the same class twice from two different class loaders, with two separate accompanying sets of dependencies, we will load all classes ourself whenever possible.
We'll want this class loader to load as many of our own dependencies as possible. The reason being, when two classes in the same package namespace have been loaded by separate class loaders, they are not considered to be belonging to the same package by the JVM. To solve this, the custom class loader will keep a local set of unusable package or class names and simply assume all of these classes belong to the system class loader.
private Set <String> _notHandled = new HashSet <String> ();
void dontHandle (String name) {
_notHandled.add(name);
}
Now to define our loadClass method.
public Class <?> loadClass (String name, boolean resolve)
throws ClassNotFoundException {
Class <?> result;
First, we will check the set of handled class/package names and see if any of them match the name we've been given. This way we can immediately pass handling on to the system class loader where it is suitable to do so.
for (String ignore : _notHandled) {
if (name.startsWith(ignore)) {
return findSystemClass(name);
}
}
After this initial check, we've determined that this class does indeed belong to us and we can continue our loading process. First we will want to transform our class name to a reasonable path for searching. We can do this fairly simply by replacing all namespace separators with file system separator characters and appending the .class suffix.
StringBuilder pathname = new StringBuilder();
pathname.append(name.replace('.', File.separatorChar))
.append(".class");
String pathtoclass = pathname.toString();
Now we're going to want to attempt to load the class from the jar this object has been initialized with. If the file does not exist in the jar it may be our test case, for example, and we will want to check the current working directory of the process for the class file. In some cases, building a test suite such as this one may require more complex logic for handling multiple jar files and so forth or building a full custom class path. But in my tests, this was perfectly fine. In the instance that the class was not found in the jar or the local path, we'll simply throw a {{{ClassNotFoundException}}} requiring the consumer to adjust their list of not-handled classes. Otherwise, we will define the class and return the instance of the Class<?> object which matches this request.
try {
JarEntry entry = _jar.getJarEntry( pathtoclass );
InputStream input;
int length;
if (entry != null) {
length = (int) entry.getSize();
input = _jar.getInputStream(entry);
}
else {
/* Try the filesystem instead! */
File file = new File( pathtoclass );
if (!file.exists()) {
throw new ClassNotFoundException(
"Unable to locate class: " + name);
}
if (!file.canRead()) {
throw new ClassNotFoundException(
"Unable to access file: " + pathname);
}
length = (int) file.length();
input = new FileInputStream( file );
}
ByteArrayOutputStream classdata =
new ByteArrayOutputStream( length );
for (int c = input.read(); c != -1; c = input.read()) {
classdata.write(c);
}
result = defineClass(name, classdata.toByteArray(), 0,
classdata.size());
input.close();
}
catch (IOException exception) {
throw new ClassNotFoundException(
"Unable to load class: " + name, exception);
}
if (resolve) resolveClass(result);
return result;
The finished result should look something like this.
import java.io.*;
import java.util.*;
import java.util.jar.*;
class UlteriorClassLoader extends ClassLoader {
private Set <String> _notHandled = new HashSet <String> ();
private JarFile _jar;
UlteriorClassLoader (File file)
throws IOException {
_jar = new JarFile(file);
}
void dontHandle (String name) {
_notHandled.add(name);
}
public Class <?> loadClass (String name)
throws ClassNotFoundException {
return loadClass(name, false);
}
public Class <?> loadClass (String name, boolean resolve)
throws ClassNotFoundException {
Class <?> result;
for (String ignore : _notHandled) {
if (name.startsWith(ignore)) {
return findSystemClass(name);
}
}
StringBuilder pathname = new StringBuilder();
pathname.append(name.replace('.', File.separatorChar))
.append(".class");
String pathtoclass = pathname.toString();
try {
JarEntry entry = _jar.getJarEntry( pathtoclass );
InputStream input;
int length;
if (entry != null) {
length = (int) entry.getSize();
input = _jar.getInputStream(entry);
}
else {
/* Try the filesystem instead! */
File file = new File( pathtoclass );
if (!file.exists()) {
throw new ClassNotFoundException(
"Unable to locate class: " + name);
}
if (!file.canRead()) {
throw new ClassNotFoundException(
"Unable to access file: " + pathname);
}
length = (int) file.length();
input = new FileInputStream( file );
}
ByteArrayOutputStream classdata =
new ByteArrayOutputStream( length );
for (int c = input.read(); c != -1; c = input.read()) {
classdata.write(c);
}
result = defineClass(name, classdata.toByteArray(), 0,
classdata.size());
input.close();
}
catch (IOException exception) {
throw new ClassNotFoundException(
"Unable to load class: " + name, exception);
}
if (resolve) resolveClass(result);
return result;
}
}
Running tests
Before any test suites can be run using this, another class will have to be written to consume this object. This class may need nothing other than a main method, but I factored out the execution of the test suite to another static method. Since many folks are quite familiar with JUnit, I used a pattern similar to its own. Simply load the class supplied in the argument list with the custom class loader, causing all of its dependency requests to happen through our loader. Then iterate over all of the methods in the class, and invoke the ones that begin with the substring test. We differ slightly from JUnit, however, and require that our tests return results that can be compared.
So to define the list of things handled by the system class loader, we'll let them pass a file name as an argument and we'll read the file in line by line and define these package/class names as "not handled". We can then guarantee that the classes defining objects we're returning as test results are loaded by the system class loader, given that such an object is available in the class path.
import java.io.*;
import java.lang.reflect.*;
class TestLoader {
static List <Object> runTestSuite (ClassLoader loader, String test)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Class cls = loader.loadClass(test);
Object object = cls.newInstance();
List <Object> results = new ArrayList <Object> ();
for (Method method : cls.getDeclaredMethods()) {
if (method.getName().startsWith("test")) {
results.add(method.invoke(object));
}
}
return results;
}
public static void main (String[] arguments)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, InvocationTargetException, IOException {
if (arguments.length < 4) {
System.err.println(
"Usage: <one.jar> <two.jar> <class-list> <classname1> " +
"<classname2>...\n" +
" Executes classname twice, loading any classes\n" +
" in the class list from the specified jars\n" +
" then compares the results of the two invocations\n"
);
System.exit(0);
}
UlteriorClassLoader firstLoader = new UlteriorClassLoader(
new File( arguments[0] ));
UlteriorClassLoader secondLoader = new UlteriorClassLoader(
new File( arguments[1] ));
BufferedReader configReader = new BufferedReader(
new FileReader( new File( arguments[2] ) ) );
while (configReader.ready()) {
String className = configReader.readLine();
firstLoader.dontHandle(className);
secondLoader.dontHandle(className);
}
List <Object> firstResults = runTestSuite(firstLoader, arguments[3]);
List <Object> secondResults = runTestSuite(secondLoader, arguments[3]);
if (firstResults.equals(secondResults)) {
System.out.println("OK");
System.exit(0);
}
System.out.println("FAILED");
System.exit(1);
}
}
Attachments
-
classloaderdiscovery.png
(13.6 kB) - added by scott
19 months ago.
Slightly Fixed version of class loader discovery diagram
-
classloaderdiagram.png
(14.0 kB) - added by scott
19 months ago.
Flow chart of the UlteriorClassLoader

