views:

561

answers:

5

I am running an executable jar and wish to find a list of classes WITHIN the jar so that I can decide at run-time which to run. It's possible that I don't know the name of the jar file so cannot unzip it

+1  A: 

I am not sure if there is a way to list all classes visible to the current classloader.

Lacking that, you could

a) try to find out the name of the jar file from the classpath, and then look at its contents.

or

b) supposing that you have a candidate list of classes you are looking for, try each of them with Class.forName().

Thilo
Thank you - it seems there is consensus that there is no simple way. FWIW I used JarFile/JarEntry to list the contents of the Jar (I knew the name) and then filtered those which were classes. When the Jar is distributed then the user may not know the name so this breaks
peter.murray.rust
A: 

you can use a simple program to get a list of all the class files from jar and dump it in a property file on runtime and then in your program you can load req. class as and when req.; without using reflections.

Priyank
A: 

You can get the actual classpath from the classloader. this must include the jar file, otherwise the program wouldn't run. Look throug the classpath URLs to find a URL that ends with ".jar" and contains something that is never changing in the name of you jar file (preferably after the last "/"). After that you open it as a regular jar (or zip) file and read the contents.

There are several methods available for obtaining the classpath. None of them works in every context and with every setup, so you must try them one by one until you find one that works in all the situations you need it to work. Also, sometimes you might need to tweak the runtime context, like (often needed) substituting maven surefire-plugin's classloading mechanism to one of optional (non-default) ones.

Obtaining the classpath 1: from system property:

static String[] getClasspathFromProperty() {
    return System.getProperty("java.class.path").split(File.pathSeparator);
}

Obtaining the classpath 2: from classloader (with maven warning):

String[] getClasspathFromClassloader() {
    URLClassLoader classLoader = (URLClassLoader) (getClass().getClassLoader());
    URL[] classpath = classLoader.getURLs();
    if (classpath.length == 1 
           && classpath[0].toExternalForm().indexOf("surefirebooter") >= 0) 
     {
        // todo: read classpath from manifest in surefireXXXX.jar
        System.err.println("NO PROPER CLASSLOADER HERE!");
        System.err.println(
             "Run maven with -Dsurefire.useSystemClassLoader=false "
            +"-Dsurefire.useManifestOnlyJar=false to enable proper classloaders");
        return null;
    }
    String[] classpathLocations = new String[classpath.length];
    for (int i = 0; i < classpath.length; i++) {
        // you must repair the path strings: "\.\" => "/" etc. 
        classpathLocations[i] = cleanClasspathUrl(classpath[i].toExternalform());
    }
    return classpathLocations;
}

Obtaining the classpath 3: from current thread context: This is similar to method 2, except the first line of the method should read like this:

    URLClassLoader classLoader 
         = (URLClassLoader)(Thread.currentThread().getContextClassLoader());

Good luck!

eirikma
A: 

I would use a bytecode inspector library like ASM. This ClassVisitor can be used to look for the main method:

import org.objectweb.asm.*;
import org.objectweb.asm.commons.EmptyVisitor;

public class MainFinder extends ClassAdapter {

  private String name;
  private boolean isMainClass;

  public MainFinder() {
    super(new EmptyVisitor());
  }

  @Override
  public void visit(int version, int access, String name,
      String signature, String superName,
      String[] interfaces) {
    this.name = name;
    super.visit(version, access, name, signature,
        superName, interfaces);
  }

  @Override
  public MethodVisitor visitMethod(int access, String name,
      String desc, String signature, String[] exceptions) {
    if ((access & Opcodes.ACC_PUBLIC) != 0
        && (access & Opcodes.ACC_STATIC) != 0
        && "main".equals(name)
        && "([Ljava/lang/String;)V".equals(desc)) {
      isMainClass = true;
    }
    return super.visitMethod(access, name, desc, signature,
        exceptions);
  }

  public String getName() {
    return name;
  }

  public boolean isMainClass() {
    return isMainClass;
  }

}

Note that you might want to alter the code to confirm that classes are public, etc.

This sample app uses the above class on a command-line-specified JAR:

import java.io.*;
import java.util.Enumeration;
import java.util.jar.*;    
import org.objectweb.asm.ClassReader;

public class FindMainMethods {

  private static void walk(JarFile jar) throws IOException {
    Enumeration<? extends JarEntry> entries = jar.entries();
    while (entries.hasMoreElements()) {
      MainFinder visitor = new MainFinder();
      JarEntry entry = entries.nextElement();
      if (!entry.getName().endsWith(".class")) {
        continue;
      }
      InputStream stream = jar.getInputStream(entry);
      try {
        ClassReader reader = new ClassReader(stream);
        reader.accept(visitor, ClassReader.SKIP_CODE);
        if (visitor.isMainClass()) {
          System.out.println(visitor.getName());
        }
      } finally {
        stream.close();
      }
    }
  }

  public static void main(String[] args) throws IOException {
    JarFile jar = new JarFile(args[0]);
    walk(jar);
  }

}

You may also want to look at the "java.class.path" system property.

System.getProperty("java.class.path");


It is possible to use reflection to obtain similar results, but that approach may have some unfortunate side-effects - like causing static initializers to be run, or keeping unused classes in memory (they will probably stay loaded until their ClassLoader is garbage collected).

McDowell
+1  A: 

You can not enumerate classes from a package of jar using Reflection API. This is also made clear in the related questions how-can-i-enumerate-all-classes-in-a-package and can-i-list-the-resources-in-a-given-package. I once wrote a tool that lists all classes found in a certain classpath. It's too long to paste here, but here is the general approach:

  1. find the used classpath. This is shown nicely by eirikma in another answer.

  2. add other places where the ClassLoader might search for classes, e.g. bootclasspath, endorsed lib in JRE etc. (If you just have a simple app, then 1 + 2 are easy, just take the class path from property.)

    • readAllFromSystemClassPath("sun.boot.class.path");
    • readAllFromSystemClassPath("java.endorsed.dirs");
    • readAllFromSystemClassPath("java.ext.dirs");
    • readAllFromSystemClassPath("java.class.path");
  3. Scan the classpath and split folders from JARs.

    • StringTokenizer pathTokenizer = new StringTokenizer(pPath, File.pathSeparator);
  4. Scan the folders with File.listFiles and open the JARs with ZipFile.entries. Pay attention to inner classes and package access classes, you propably do not want them.

    • isInner = (pClassName.indexOf('$') > -1);
  5. Convert the file name or path in the JAR to a proper classname (/ -> .)

    • final int i = fullName.lastIndexOf(File.separatorChar);
    • path = fullName.substring(0, i).replace(File.separatorChar, '.');
    • name = fullName.substring(i + 1);
  6. Now you can use Reflection to load that class and have a look into it. If you just want to know stuff of about the class you can load it without resolving, or use a byte code engineering library like BCEL to open the class without loading it into the JVM.

    • ClassLoader.getSystemClassLoader().loadClass(name).getModifiers() & Modifier.PUBLIC
Peter Kofler