The CTP JarClassLoader

From MircWiki
Revision as of 19:22, 28 May 2009 by Johnperry (talk | contribs)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

This article describes the startup mechanism used by CTP. Nobody needs to read this article in order to use, or even to extend, CTP. The purpose is simply to capture the thought process in case changes are contemplated in the future. This article contains highly technical, secret information which will make you extremely attractive to women, which as all programmers know, is a heavy burden to bear. Proceed at your own risk.

CTP is a pipeline processor which is designed to be extensible. The idea is that programmers can implement new PipelineStages or database interfaces and couple them into the program without having to modify CTP itself. See Extending CTP for more information. Once an extension has been created, it must be recognized by CTP in order to be available for loading. The recommended way to do that is to build extensions into JAR files and place those files in the libraries directory of the CTP installation. To support this mechanism, CTP must have a way to load classes that are not on the classpath.

A brief diversion on ClassLoaders

ClassLoaders load classes and resources. There are three standard ClassLoaders in Java:

  • The primordial ClassLoader is responsible for loading the classes of the Java language. This ClassLoader is part of the JVM, and it is written in native code for the platform.
  • The extension ClassLoader is responsible for loading Java extensions. These are typically classes in JARs in the JVM/lib/ext directory, although it is possible to specify additional extensions directories when starting a Java program.
  • The application ClassLoader is responsible for loading classes found on the classpath.

The classpath is defined by an environment variable or an attribute in the manifest of a JAR file. It can also be specified on the command line when starting a Java program.

A ClassLoader (except for the primordial ClassLoader) is a Java class, and it must be loaded by another ClassLoader, which becomes its parent. ClassLoaders typically obey delegation rules along these lines:

  1. When asked to load a class (by a call to its loadClass method), the ClassLoader first checks to see if it has already loaded the class. If so, it must return exactly the same Class object for it that it returned before.
  2. If the requested class has not been previously loaded, the ClassLoader calls the loadClass method of its parent. If the parent returns a Class object for the requested class, the ClassLoader returns it.
  3. If the parent failed to load the requested class (indicated by its receiving a ClassNotFoundException from the parent), the ClassLoader searches its domain to find the class. If it finds the class, it returns it. If not, it throws a ClassNotFoundException.

The effect of the delegation mechanism is that the highest level ClassLoader which can load a class is the one that loads it. Every Class object knows the ClassLoader which loaded it. When an object attempts to instantiate another object using the new instruction, the instantiating object's ClassLoader is used to load the Class of the instantiated class, but the delegation mechanism may result in a higher level ClassLoader being the one that does the actual loading (and being the one that the loaded class knows as its ClassLoader). This little bit of arcana will be significant later.

Parenthetic note: For two Class objects to be equal, they must be the same class and they must have been loaded by the same ClassLoader.

CTP startup

CTP is packaged in the CTP.jar file. The main class is org.rsna.ctp.ClinicalTrialProcessor. When the program starts, the main method of the main class is called. That method is shown below.

static final File libraries = new File("libraries");
static final String ctp = "org.rsna.ctp.ClinicalTrialProcessor";

public static void main(String[] args) {

  //Make sure the libraries directory is present
  libraries.mkdirs();

  //Get a JarClassLoader pointing to this program plus the libraries directory
  JarClassLoader cl = 
    JarClassLoader.getInstance(new File[] { new File("CTP.jar"), libraries });

  //Set the context classloader to the JarClassLoader
  Thread.currentThread().setContextClassLoader(cl);

  //Load the class and instantiate it
  try {
    Class ctpClass = cl.loadClass(ctp);
    ctpClass.getConstructor( new Class[0] ).newInstance( new Object[0] );
  }
  catch (Exception unable) { unable.printStackTrace(); }
}

The key objective of the main method is to get the program started, with a ClassLoader in place which knows about the JARs in the libraries directory. There are several subtleties in this method, so it is worthwhile to examine each instruction.

  //Make sure the libraries directory is present
  libraries.mkdirs();

This simply ensures that the libraries directory exists.

  //Get a JarClassLoader pointing to this program plus the libraries directory
  JarClassLoader cl = 
    JarClassLoader.getInstance(new File[] { new File("CTP.jar"), libraries });

This gets an instance of a JarClassLoader which knows about the CTP.jar file (which is located in the top-level CTP directory) and all the JAR files located in the libraries directory. This is a ClassLoader with a special delegation feature that takes precedence over its parent ClassLoader, thus ensuring that if it can load a class it becomes the class's ClassLoader and not one of the parent ClassLoaders. The JarClassLoader is described in detail below.

  //Set the context classloader to the JarClassLoader
  Thread.currentThread().setContextClassLoader(cl);

This sets the context ClassLoader of the Thread in which the program is starting to the JarClassLoader. This is necessary in the case of CTP because the DICOM library (dcm4che) has certain methods which use the context ClassLoader to load classes which are in JARs in the libraries directory. When the program starts, the context ClassLoader is the Application classLoader, which doesn't know about those JARs, so it is necessary to replace it with one that does.

  //Load the class and instantiate it
  try {
    Class ctpClass = cl.loadClass(ctp);
    ctpClass.getConstructor( new Class[0] ).newInstance( new Object[0] );
  }
  catch (Exception unable) { unable.printStackTrace(); }

This instantiates the ClinicalTrialProcessor class. Note that when the program starts, the ApplicationClassLoader is the one which loaded the main class, so it would be the one that is used if an instruction like the following were used:

    new ClinicalTrialProcessor();

This would have the effect of making the ApplicationClassLoader the ClassLoader for the entire application, so it is necessary to load the class again with the JarClassLoader to ensure that the JarClassLoader becomes the ClassLoader for everything.

Important note: If the JarClassLoader obeyed the normal delegation rules, then it would call the parent ClassLoader (the ApplicationClassLoader), and that would try to load the ClinicalTrialProcessor class. Since the ApplicationClassLoader had already loaded the class when the program was started, it would return the same Class object, which would have as its ClassLoader the ApplicationClassLoader. This would subvert the whole intent. Thus, it is imperative that the JarClassLoader use a reversed delegation model.

The java.net.URLClassLoader

The java.net package has a URLClassLoader which loads classes from an array of URLs pointing to JARs. Like all standard ClassLoaders, it obeys the standard delegation rules.


... more to come ...