The Source for Java Technology Collaboration
User: Password:



Laird Nelson's Blog

J2SE Archives


Cheap Hack I: rename your jar file, get a different Main-Class

Posted by ljnelson on September 20, 2004 at 12:09 PM | Permalink | Comments (4)

Here's a fun hack.

I (like everyone else in the world) have a collection of utilities I take with me from job to job. It's served me well for several years now. These utilities are simple, domain-independent things: classes to copy files, find out what jar file a class is loading from, that sort of thing.

Many of the classes in this utility collection jar have main() methods, and could conceivably serve as the Main-Class in an executable jar somewhere. But I don't want to get into the business of fragmenting my utility jar. What to do?

Feeling hackish, I put together a class that itself is installed as the Main-Class in a jar file, but which consults a file (also in the jar) to let it know what class to actually use as the main class. The hackish part is that all you have to do is rename the jar file for the "right" Main-Class to be selected. Here's the (quick, dirty, uncommented) code:

import java.io.InputStream;
import java.io.IOException;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.net.URL;

import java.util.Properties;

public final class Main {

  private Main() {
    super();
  }

  public static final void main(final String[] args) throws Throwable {
    final URL location;
    final String classLocation = Main.class.getName().replace('.', '/') + ".class";
    final ClassLoader loader = Main.class.getClassLoader();
    if (loader == null) {
      location = ClassLoader.getSystemResource(classLocation);
    } else {
      location = loader.getResource(classLocation);
    }
    String token = null;
    if (location != null && "jar".equals(location.getProtocol())) {
      String urlString = location.toString();
      if (urlString != null) {
        final int lastBangIndex = urlString.lastIndexOf("!");
        if (lastBangIndex >= 0) {
          urlString = urlString.substring("jar:".length(), lastBangIndex);
          if (urlString != null) {
            final int lastSlashIndex = urlString.lastIndexOf("/");
            if (lastSlashIndex >= 0) {
              token = urlString.substring(lastSlashIndex + 1);
            }
          }
        }
      }
    }
    if (token != null) {
      InputStream stream = null;
      try {
        if (loader == null) {
          stream = ClassLoader.getSystemResourceAsStream("mainClasses");
        } else {
          stream = loader.getResourceAsStream("mainClasses");
        }
        if (stream != null) {
          final Properties properties = new Properties(System.getProperties());
          properties.load(stream);
          final String mainClassName = properties.getProperty(token + ".main.class");
          if (mainClassName != null) {
            final Class mainClass = Class.forName(mainClassName);
            if (mainClass != null) {
              final Method mainMethod = mainClass.getDeclaredMethod("main", new Class[] { args.getClass() });
              if (mainMethod != null && Void.TYPE.equals(mainMethod.getReturnType())) {
                final int modifiers = mainMethod.getModifiers();
                if (Modifier.isPublic(modifiers) &&
                    Modifier.isStatic(modifiers)) {
                  try {
                    mainMethod.invoke(null, new Object[] { args });
                  } catch (final InvocationTargetException kaboom) {
                    throw kaboom.getTargetException();
                  }
                }
              }
            }
          }
        }
      } finally {
        if (stream != null) {
          try {
            stream.close();
          } catch (final IOException ignore) {
            // ignore
          }
        }
      }
    }
  }

}

There are two interesting bits to this. The first is the way that a class' location is returned as a URL. Suppose you have a class, com.foo.bar.Baz. And suppose that class is present in a jar file whose (Windows, let's say) path is C:\x\y\z.jar. Then if you say:

URL url =
classLoader.getResource("com/foo/bar/Baz.class");
...the URL you'll get back is:
jar:file:C:/x/y/z.jar!/com/foo/bar/Baz.class
This format, which is documented as part of the java.net.JarURLConnection class, is remarkably useful. The URL format, as you can see, gives you both the directory structure of the jar file itself, as well as the location of the jar file. In the code above, we use this URL format to figure out what the unqualified name of the jar file is--the "basename":
String urlString = location.toString();
if (urlString != null) {
  final int lastBangIndex = urlString.lastIndexOf("!");
  if (lastBangIndex >= 0) {
    urlString = urlString.substring("jar:".length(), lastBangIndex);
    if (urlString != null) {
      final int lastSlashIndex = urlString.lastIndexOf("/");
      if (lastSlashIndex >= 0) {
        token = urlString.substring(lastSlashIndex + 1);
      }
    }
  }
}
Here we simply lop off everything from the "jar:" part up to and including the last slash (well, the last slash before the "!" delimiter). Then we lop off everything from (and including) the "!" delimiter to give us the "basename" of the jar file, which, in our contrived example, would simply be z.jar.

Armed with that as a key, we now go looking for a well-known file inside the jar. I simply called it "mainClasses". It is a simple Properties dump, and is accessible via the usual getResourceAsStream() machinery:

stream =
loader.getResourceAsStream("mainClasses");
(If you want to be really careful, you would actually open the jar file yourself and extract this entry from it. The problem with the way we've done it here is that if there is any file in the classpath named mainClasses that appears before the jar file in question, it will be selected instead.)

If we found it, we load a new Properties object from it, where, hopefully, the keys are jar "basenames", and the values are fully qualified names of classes, each of which will serve as that jar file's Main-Class. Here's an example of the contents of that file:

z.jar: com.foo.bar.Xyzzy
q.jar: com.foo.bar.Frobnicator
This file indicates that if the jar is named z.jar, then its main class will be com.foo.bar.Xyzzy. If, on the other hand, the jar is named q.jar, then its main class will be com.foo.bar.Frobnicator.

The rest of the code is simply the process of loading the correct class via reflection and seeing if it has a public static void main(String[]) method on it. Note that the only kind of Exception we explicitly catch is InvocationTargetException so that if something goes wrong in the delegate main class, it will look (as much as possible) as though that class were directly invoked.

Obviously this dirty little hack will only scratch an itch if it makes sense to copy the jar file to multiple places with different names. In my case it does, because my utility jar file is very small.





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds