Skip to main content

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

Posted by ljnelson on September 20, 2004 at 12:09 PM PDT

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 href="http://java.sun.com/j2se/1.3.1/docs/api/java/net/JarURLConnection.html">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.

Related Topics >>