简体   繁体   中英

Deleting a jar from another jar

I have two jar files, Foo.jar and Bar.jar . Inside Foo.jar I have a class Foo with a main method which looks like this:

public class Foo {
  public static void main(String[] args) {
    Bar bar = new Bar();
    bar.sayHello();
    new File("./Bar.jar").deleteOnExit();
  }
}

The class Bar lives in Bar.jar and looks like this:

public class Bar {
  public void sayHello() {
    System.out.println("Hello! I'm Bar!");
  }
}

The intention here is that Foo.jar will use a class in Bar.jar and afterwards it will delete Bar.jar . With the above implementation this is the output when executed:

$ java -jar Foo.jar
Hello! I'm Bar!

but Bar.jar does not get deleted. Here I've tried both new File("./Bar.jar").deleteOnExit(); and new File("./Bar.jar").delete(); in class Foo .

If I change class Bar to close its ClassLoader like this:

public class Bar {
  public void sayHello() {
    System.out.println("Hello! I'm Bar!");

    URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
    try {
      classLoader.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

and run Foo.jar again, this is the output:

$ java -jar Foo.jar
Hello! I'm Bar!

the output is the same as in the previous example, but in this case Bar.jar is deleted, which is great, but the problem now is that if I do something like this in class Foo :

9   public class Foo {
10    public static void main(String[] args) {
11      Bar bar = new Bar();
12      bar.sayHello();
13    
14      new File("./Bar.jar").deleteOnExit();
15    
16      SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
17      };
18    }
19  }

this becomes the output of executing Foo.jar :

$ java -jar Foo.jar
Hello! I'm Bar!
Exception in thread "main" java.lang.NoClassDefFoundError: playground/modulae/Foo$1
        at playground.modulae.Foo.main(Foo.java:16)
Caused by: java.lang.ClassNotFoundException: playground.modulae.Foo$1
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more

In this case, Bar.jar still gets deleted, but when I try to create a new SimpleFileVisitor it crashes with a NoClassDefFoundError . It seems like closing the ClassLoader in Bar.jar causes things to get a bit messed up for Foo.jar as well, but I have not found any other way that lets me delete Bar.jar from Foo.jar .

The general structure of this example closely resembles what happens in the real application, which uses java.nio.file.SimpleFileVisitor later on. I've tried creating a few other classes instead of java.no.file.SimpleFileVisitor and none of them crash, but I guess it depends on whether or not they have been loaded by the ClassLoader before Foo calls Bar .


Update

After messing around with class loaders (which I still don't fully understand) I have something that seems to be sufficient. However, I fear that I'm doing something a bit dodgy and that there is a more 'correct' way of handling the class loader manipulation that I'm doing.

So the setup now is:

  1. I have class Foo with a main method and a speak() method in Foo.jar .
  2. A class Bar with a simple sayHello() method in Bar.jar .
  3. In the main function I create an instance foo of Foo with a custom class loader and call foo.speak()
  4. In foo.speak() I create a instance bar of Bar like this: Bar bar = new Bar() <- Notice that there is no reflection or anything fancy here. Then I call bar.sayHello()
  5. In bar.sayHello() I call this.getClass().getClassLoader().close()
  6. Everything then returns to main which deletes Bar.jar

And then the code:

Foo.java (in Foo.jar)

public class Foo {
  public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    System.out.println("Foo.main: Class path: " + System.getProperty("java.class.path"));

    URLClassLoader initialClassLoader = (URLClassLoader) Foo.class.getClassLoader();
    System.out.println("Foo.main: initialClassLoader = " + initialClassLoader);
    System.out.println("Foo.main: initialClassLoader.getURLs() = " + Arrays.toString(initialClassLoader.getURLs()));

    File fooJar = new File("./Foo.jar");
    File barJar = new File("./Bar.jar");

    CustomClassLoader customClassLoader = new CustomClassLoader(new URL[]{fooJar.toURI().toURL(), barJar.toURI().toURL()});
    System.out.println("Foo.main: customClassLoader = " + customClassLoader);
    System.out.println("Foo.main: customClassLoader.getURLs() = " + Arrays.toString(customClassLoader.getURLs()));
    Class<?> fooClass = customClassLoader.findClass("Foo");
    Object foo = fooClass.newInstance();
    Method speak = fooClass.getMethod("speak");
    speak.invoke(foo);

    barJar.deleteOnExit();

    SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
    };
  }

  public void speak() {
    System.out.println("Foo.speak()");
    URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
    System.out.println("Foo.speak(): classLoader = " + classLoader);
    System.out.println("Foo.speak(): classLoader.getURLs() = " + Arrays.toString(classLoader.getURLs()));

    Bar bar = new Bar();
    bar.sayHello();
  }
}

Bar.java (in Bar.jar)

public class Bar {
  public void sayHello() {
    System.out.println("Bar.sayHello()");
    URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
    System.out.println("Bar.sayHello(): classLoader = " + classLoader);
    System.out.println("Bar.sayHello(): classLoader.getURLs() = " + Arrays.toString(classLoader.getURLs()));

    try {
      classLoader.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

CustomClassLoader (in Foo.jar)

public class CustomClassLoader extends URLClassLoader {      
  public CustomClassLoader(URL[] urls) {
    super(urls);
  }

  @Override
  public Class<?> findClass(String name) throws ClassNotFoundException {
    return super.findClass(name);
  }
}

Output

$ java -jar Foo.jar
Foo.main: Class path: Foo.jar
Foo.main: initialClassLoader = sun.misc.Launcher$AppClassLoader@1f96302
Foo.main: initialClassLoader.getURLs() = [file:/Foo.jar]
Foo.main: customClassLoader = CustomClassLoader@a298b7
Foo.main: customClassLoader.getURLs() = [file:/Foo.jar, file:/Bar.jar]
Foo.speak()
Foo.speak(): classLoader = CustomClassLoader@a298b7
Foo.speak(): classLoader.getURLs() = [file:/Foo.jar, file:/Bar.jar]
Bar.sayHello()
Bar.sayHello(): classLoader = CustomClassLoader@a298b7
Bar.sayHello(): classLoader.getURLs() = [file:/Foo.jar, file:/Bar.jar]

and Bar.jar is deleted.

Thoughts

As we see, Bar can be referenced from within Foo.jar without any reflection, it has been loaded with the correct CustomClassLoader and Bar.jar can successfully be deleted from Foo after having called bar.sayHello() .

Now, this solves my problem. However, I'm not confident that I have solved the CustomClassLoader correctly. In fact, the only thing that CustomClassLoader effectively does is make findClass() public. I tried a whole bunch of different ways of implementing loadClass() , but could never get it to work and just by accident I tried findClass() instead and it worked.

Does anyone have any opinions on how CustomClassLoader is used?

OK, so here what I would do:

I would define an interface to use by Foo , and implement by Bar :

public interface BarIntf {
    public void sayHello();
}

The interface must be in Foo.jar.

The Bar class would implement BarIntf :

public class Bar implements BarIntf {
    @Override
    public void sayHello() {
        System.out.println("Hello! I'm Bar!");
    }
}

Now, instead of putting both Foo.jar and Bar.jar in the classpath, I would put only Foo.jar, and create a class loader in program Foo just to load the Bar class.

It looks like this:

public class Foo {
    public static void main(String[] args) {
        File jar = new File("./Bar.jar");
        try (URLClassLoader cl = new URLClassLoader(
                new URL[] {jar.toURI().toURL()})) {
            Class<?> clazz = cl.loadClass("bar.Bar");
            BarIntf bar = (BarIntf)clazz.newInstance();
            bar.sayHello();
            // ...a lot of very useful things
        } catch (IOException | ClassNotFoundException
                | InstantiationException | IllegalAccessException ex) {
            Logger.getLogger(Foo.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            jar.delete();
        }
    }
}

UPDATE:

With reflection (no interface required):

public class Foo {
    public static void main(String[] args) {
        File jar = new File("./Bar.jar");
        try (URLClassLoader cl = new URLClassLoader(
                new URL[] {jar.toURI().toURL()})) {
            Class<?> clazz = cl.loadClass("bar.Bar");
            Method method = clazz.getDeclaredMethod("sayHello");
            Object bar = clazz.newInstance();
            method.invoke(bar);
            // ...a lot of very useful things
        } catch (IOException | ClassNotFoundException
                | InstantiationException | IllegalAccessException
                | NoSuchMethodException | SecurityException
                | InvocationTargetException ex) {
            Logger.getLogger(Foo.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            jar.delete();
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM