In order to mask the complexity of starting the Java application, I decided that Jetty had the right approach: creating an executable JAR that was used in lieu of a regular startup script (or batch file). It was pretty easy overall, though I had to peek at Jetty’s source to be reminded of the existence of URLClassLoader.
The idea is to create a simple starter class in an executable JAR, which instantiates a custom ClassLoader used to load the real classes for the program. It also does any other tedious setup work, like setting properties or starting the logging system (via reflection, b/c the custom classloader will have the classes, rather than the starter’s ClassLoader).
This lets me do any custom dynamic configuration I want. I load all the JAR libraries from the “lib” subdirectory, and use those classes as the CLASSPATH to run the real program. Since any JAR file is loaded, upgrading is as simple as deleting a JAR and dropping in a new one; the name of the new JAR and old JAR doesn’t matter. Having name not matter is useful to me, as the JAR files I build are versioned and timestamped in their name for easy recognition.
Admittedly, I could do all this using a script, and dynamically building the command line used to invoke the Java runtime. However, I’d have to write at least two scripts (Windows and UNIX), and I’d have to either write for the lowest common denominator toolset I can assume is present on the system (e.g. grep, sed, VBScript, Windows CMD scripts) , or bundle my own (e.g. Perl). With a Java starter, I’m guaranteed access to the Java runtime, which is a very functional “lowest common denominator”. Besides, why bother working with a script language to specify Java-centric ideas, when you can use Java to specify Java-centric notions?
In case you’re curious what I did:
import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; public final class Start { public static void main(String[] args) { try { // Search for the Start class that's in a JAR from the library, rather // than the Start class that we're currently using. getlibraryClassLoader().loadClass(Start.class.getName()).getMethod( "start", new Class[] { String[].class }).invoke(null, new Object[] { args }); } catch (Exception e) { e.printStackTrace(); } } public static void start(String[] args) { try { ClassLoader loader = Start.class.getClassLoader(); // Jetty uses the ClassLoader assigned to the thread to load classes for // the servlets. Thread.currentThread().setContextClassLoader(loader); Class serverClass = loader.loadClass("org.mortbay.jetty.Server"); Class listenerClass = loader.loadClass("org.mortbay.http.SocketListener"); Object server = serverClass.newInstance(); Object listener = listenerClass.newInstance(); listenerClass.getMethod("setPort", new Class[] { int.class }).invoke( listener, new Object[] { PORT }); serverClass.getMethod("addWebApplication", new Class[] { String.class, String.class }).invoke(server, new Object[] { WEBAPP_CTX, WEBAPP_DIR }); // addListener() and start() aren't defined on org.mortbay.jetty.Server, // but defined on its base classes. loader.loadClass("org.mortbay.http.HttpServer").getMethod("addListener", new Class[] { loader.loadClass("org.mortbay.http.HttpListener") }) .invoke(server, new Object[] { listener }); loader.loadClass("org.mortbay.util.Container").getMethod("start", new Class[0]).invoke(server, new Object[0]); } catch (Exception e) { e.printStackTrace(); } } public static ClassLoader getResourceClassLoader() { if (g_resourceClassLoader == null) { try { g_resourceClassLoader = new URLClassLoader( new URL[] { RESOURCE_DIRECTORY.toURL() }, null); } catch (MalformedURLException e) { throw new RuntimeException("Couldn't create resource ClassLoader.", e); } } return g_resourceClassLoader; } public static ClassLoader getlibraryClassLoader() { if (g_libraryClassLoader == null) { try { ArrayList urlList = new ArrayList(); // Search the JAR directory for anything that looks like a JAR and add // it to the ClassLoader's search path. for (String childFilename : LIBRARY_DIRECTORY.list()) { File child = new File(LIBRARY_DIRECTORY, childFilename); if (child.isFile() && childFilename.matches(".*\\.jar$")) { urlList.add(child.toURL()); } } // The configuration ClassLoader is the parent fo the URLClassLoader, // mostly because Log4J automatically searches for "log4j.properties" on // its first use. g_libraryClassLoader = new URLClassLoader(urlList .toArray(new URL[urlList.size()]), getConfigurationClassLoader()); } catch (MalformedURLException e) { throw new RuntimeException("Couldn't create library ClassLoader.", e); } } return g_libraryClassLoader; } public static ClassLoader getConfigurationClassLoader() { if (g_configurationClassLoader == null) { try { g_configurationClassLoader = new URLClassLoader( new URL[] { CONFIGURATION_DIRECTORY.toURL() }, null); } catch (MalformedURLException e) { throw new RuntimeException("Couldn't create confiuration ClassLoader.", e); } } return g_configurationClassLoader; } private Start() {} private static URLClassLoader g_libraryClassLoader; private static URLClassLoader g_resourceClassLoader; private static URLClassLoader g_configurationClassLoader; private final static int PORT = 8080; private static final String WEBAPP_DIR = "web/"; private final static String WEBAPP_CTX = "/"; private final static File CONFIGURATION_DIRECTORY = new File("config"); private final static File LIBRARY_DIRECTORY = new File("lib"); private final static File RESOURCE_DIRECTORY = new File("resource"); }