views:

241

answers:

4

I'm developing an application that dynamically loads a JAR, which contains the definition of a bunch of classes it uses. Everything went fine until I tried to catch an Exception-derived class that's in the dynamically loaded JAR.

The following snippet shows the issue (DynamicJarLoader is the class which actually loads the JAR; both TestClass and MyException are in the external JAR):

public static void main(String[] args) {
    DynamicJarLoader.loadFile("../DynamicTestJar.jar");
    try {
         String foo = new TestClass().testMethod("42");
    } catch(MyException e) { }
}

When I try to run it, I get this:

Exception in thread "main" java.lang.NoClassDefFoundError: dynamictestjar/MyException
Caused by: java.lang.ClassNotFoundException: dynamictestjar.MyException
  at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
  at java.security.AccessController.doPrivileged(Native Method)
  at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
  at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
Could not find the main class: dynamicjartestapp.Main. Program will exit.

If I replace catch(MyException e) with catch(Exception e), the program runs fine. This means that Java is able to find TestClass after the JAR has already been loaded. So it appears that the JVM needs all Exception classes to be defined when the program starts running, and not when they're needed (i.e. when that particular try-catch block is reached).

Why does this happen?

EDIT

I've run some additional tests, and this is indeed quite odd. This is the full source of MyException:

package dynamictestjar;
public class MyException extends RuntimeException {
 public void test() {
  System.out.println("abc");
 }
}

This code runs:

public static void main(String[] args) {
    DynamicJarLoader.loadFile("../DynamicTestJar.jar");
    String foo = new TestClass().testMethod("42");
    new MyException().test(); //prints "abc"
}

This doesn't:

  public static void main(String[] args) {
    DynamicJarLoader.loadFile("../DynamicTestJar.jar");
    String foo = new TestClass().testMethod("42");
    new MyException().printStackTrace(); // throws NoClassDefFoundError
  }

I should point out that whenever I run my tests from NetBeans, everything goes according to plan. The weirdness begins only when I forcefully remove the external Jar from Java's eyes and run the test app from the command line.

EDIT #2

Based on the answers, I wrote this, which I think proves the one I accepted is indeed right:

  public static void main(String[] args) {
    DynamicJarLoader.loadFile("../DynamicTestJar.jar");
    String foo = new TestClass().testMethod("42");
 class tempClass {
  public void test() {
   new MyException().printStackTrace();
  }
 }
 new tempClass().test(); // prints the stack trace, as expected
  }
+5  A: 

"So it appears that the JVM needs all Exception classes to be defined when the program starts running, and not when they're needed" - no, I don't think this is correct.

I'm betting that your problem is due to these potential errors on your part, none of which have anything to do with the JVM:

  1. You're missing a package statement at the top of your MyException.java file. It looks like you meant it to be "package dynamictestjar", but it's not there.
  2. You did have the correct package statement in your MyException.java file, but when you compiled you didn't end up with the MyException.class file in a folder named "dynamictestjar".
  3. When you packaged your code into DynamicTestJar.jar (Star Wars fan, you are) you didn't get the path right because you didn't ZIP up the directory containing the folder "dynamictestjar", so the path that the class loader sees is incorrect.

None of these are due to great mysteries about the class loader. You need to check and make sure that you're doing your stuff properly.

Why do you need to load the JAR dynamically like this? What are you doing that couldn't be accomplished by simply putting the JAR in the CLASSPATH and letting the class loader pick it up?

I hope this is just an example and not indicative of your "best practices" in Java:

catch(MyException e) { }

You should at least print the stack trace or use Log4J to log the exception.

UPDATE:

I should point out that whenever I run my tests from NetBeans, everything goes according to plan. The weirdness begins only when I forcefully remove the external Jar from Java's eyes and run the test app from the command line.

This suggests that the path you've hard-wired into the path to the JAR isn't satisfied when you run from the command line.

JVM doesn't work one way with NetBeans and another way without it. It's all about understanding CLASSPATH, the class loader, and path issues. When you sort yours out, it'll work.

duffymo
Thanks for answering! It's possible I screwed something up, but consider these two facts: i) `TestClass` is found, and it is in the same package as `MyException`. I've just checked the jar file, and `MyException.class` is where it's supposed to be. ii) When I leave `DynamicTestJar.jar` where Java can find it (e.g. lib/), the program runs (just checked it). It also runs from NetBeans. The problem only manifests itself when I delete the jar and put it somewhere Java doesn't look (in this case, "..").
Pedro d'Aquino
Oh, yes, I'm not swallowing Exceptions like this in real code! This is just the simplest possible snippet that illustrates the problem, but thanks for the heads up.
Pedro d'Aquino
"...where Java can find it..." - there is no assumption about looking in a /lib directory built into Java. God forbid, you're not adding your code to the JDK directories, are you? Believe me Pedro, it's still you. Your code and configuration is still the problem. Don't take success at finding TestClass as an indication that you're "correct". When you get it right, the JVM will find your exception and that CNF problem will go away.
duffymo
@duffymo: No, I'm not :) Perhaps Java's looking in the "lib" directory as a result of building it with NetBeans?
Pedro d'Aquino
I don't know - I use IntelliJ.
duffymo
+3  A: 

Is MyException an exception class in the JAR that you are loading dynamically?

Note that you are statically using class MyException, as you have it literally in your code.

Are you putting DynamicTestJar.jar in your classpath while you're compiling, but not while you're running the program? Don't put it in the classpath while compiling, so that the compiler will show you where you're using the JAR in a static way.

Jesper
"statically"? Not sure what you mean by that. He's calling "new" to create an instance and invoking methods on it.
duffymo
@duffymo: when the class containing the `main` method is loaded it will try to load the `MyException` class and fail because the code that dynamically loads the jar containing `MyException` won't have been run yet. That's what Jesper means by 'statically using'.
Nick Holt
+1 - nice spot Jesper
Nick Holt
@jesper, Nick: As my recent edits show, this also happens when I instantiate `MyException`. Does that make any difference?
Pedro d'Aquino
to recap this answer (which is basically correct), you should not have the "dynamic" jar in your compilation classpath. any class you need in the compile classpath, you will need to be available when the class is first loaded (i.e. you cannot load it "dynamically" after the main class loads).
james
Could you elaborate a bit more (perhaps post a more detailed answer)? Thanks!
Pedro d'Aquino
@Pedro: yes, both instantiations should work (don't know why printStackTrace isn't). However if you look at the bytecode (javap -c -private *your class*) for an example where you try to catch the exception you'll see there's an section called `Exception table`. Now this is a guess (and Googling hasn't turned up a definitive answer) but it wouldn't surprise me if the classes in the `Exception table` were loaded before the body of the code is executed.
Nick Holt
see my answer in the original question. if you need to catch MyException in your main method, put it in your base jar. as a general rule, do not compile your base jar classes with the dynamic jar classes in the classpath. this will help you avoid the issue in the future.
james
+1 James for the sound advice.
Nick Holt
+1  A: 

You're loading the JAR DynamicTestJar.jar dynamically at runtime but you add it to the classpath when you compile the code.

So when the default classloader tries to load the bytecode for main(), it can't find MyException on the classpath and throws an exception. This happens before `DynamicJarLoader.loadFile("../DynamicTestJar.jar"); is executed!

So you must make sure that the classes from your dynamic JAR are available in the currently active classloader when a class is loaded that needs them. You can'd add the JAR to the classpath afterwards, especially not in a class that imports something from it.

Aaron Digulla
A: 

In Java, all classes are uniquely identified by classloader and FQN of the class.

ClassLoaders are hierarchical, meaning your dynamically loaded class has access to all of its parent's classes, but the parent doesn't have any direct access to its classes.

Now, if your child classloader defines a class that extends your parent, your parent's code can refer to that class only through reflection, or by the parent class.

If you think of all Java code as reflective, this becomes easier. IE:

Class.forName("MyException")

The current class doesn't have access to the the child classloader, so it can't do this.

Eric Anderson