1

The goal is to have a simple Java class start a jar main-class. When the main-class finishes, it can be queried if it would like to be reloaded. By this method it can hot-update itself and re-run itself.

The Launcher is to load the jar via a URLClassloader and then unload/re-load the changed jar. The jar can be changed by a modification to Launcher, or by a provided dos/unix script that is called to swap the new jar into place of the old jar.

The entire program is below. Tested and it seems to work without a hitch.

java Launcher -jar [path_to_jar] -run [yourbatchfile] [-runonce]? optional]

1) Launcher looks for the "Main-Class" attribute in your jarfile so that it does not need to be giventhe actual class to run.

2) It will call 'public static void main(String[] args)' and pass it the remaining arguments that you provided on the command line

3) When your program finishes, the Launcher will call your programs method 'public static boolean reload()' and if the result is 'true' this will trigger a reload.

4) If you specified -runonce, then the program will never reload.

5) If you specified -run [batchfile] then the batchfile will run before reloading.

I hope this is helpful to some people.

Happy coding!

import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes;
import java.util.jar.Manifest;


public class Launcher {

    public static void main(String[] args) {
        new Launcher().run(new ArrayList<>(Arrays.asList(args)));
    }

    private void run(List<String> list) {
        final String jar = removeArgPairOrNull("-jar", list);
        final boolean runonce = removeArgSingle("-runonce", list);
        final String batchfile = removeArgPairOrNull("-run", list);

        if (jar == null) {
            System.out.println("Please add -jar [jarfile]");
            System.out.println("All other arguments will be passed to the jar main class.");
            System.out.println("To prevent reloading, add the argument to -runonce");
            System.out.println("To provide another program that runs before a reload, add -run [file]");
        }

        boolean reload;

        do {
            reload = launch(list.toArray(new String[0]), new String(jar), new String(batchfile), new Boolean(runonce));
            System.out.println("Launcher: reload is: " + reload);

            gc();

            if (reload && batchfile != null) {
                try {
                    System.err.println("Launcher: will attempt to reload jar: " + jar);
                    runBatchFile(batchfile);
                } catch (IOException | InterruptedException ex) {
                    ex.printStackTrace(System.err);
                    System.err.println("Launcher: reload batchfile had exception:" + ex);
                    reload = false;
                }
            }

        } while (reload);

    }

    private boolean launch(String[] args, String jar, String batchfile, boolean runonce) {

        Class<?> clazz = null;
        URLClassLoader urlClassLoader = null;
        boolean reload = false;

        try {
            urlClassLoader = new URLClassLoader(new URL[]{new File(jar).toURI().toURL()});

            String mainClass = findMainClass(urlClassLoader);
            clazz = Class.forName(mainClass, true, urlClassLoader);

            Method main = clazz.getMethod("main", String[].class);
            System.err.println("Launcher: have method: " + main);

            Method reloadMethod;

            if (runonce) {
                // invoke main method using reflection.
                main.invoke(null, (Object) args);
            } else {
                // find main and reload methods and invoke using reflection.
                reloadMethod = clazz.getMethod("reload");

                main.invoke(null, (Object) args);
                System.err.println("Launcher: invoked: " + main);

                reload = (Boolean) reloadMethod.invoke(null, new Object[0]);
            }
        } catch (final Exception ex) {
            ex.printStackTrace(System.err);
            System.err.println("Launcher: can not launch and reload this class:" + ex);
            System.err.println("> " + clazz);
            reload = false;
        } finally {
            if (urlClassLoader != null) {
                try {
                    urlClassLoader.close();
                } catch (IOException ex) {
                    ex.printStackTrace(System.err);
                    System.err.println("Launcher: error closing classloader: " + ex);
                }
            }
        }

        return reload ? true : false;
    }

    private static String findMainClass(URLClassLoader urlClassLoader) throws IOException {
        URL url = urlClassLoader.findResource("META-INF/MANIFEST.MF");
        Manifest manifest = new Manifest(url.openStream());
        Attributes attr = manifest.getMainAttributes();
        return attr.getValue("Main-Class");
    }

    private static void runBatchFile(String batchfile) throws IOException, InterruptedException {
        System.out.println("Launcher: executng batchfile: " + batchfile);
        ProcessBuilder pb = new ProcessBuilder("cmd", "/C", batchfile);
        pb.redirectErrorStream(true);
        pb.redirectInput(Redirect.INHERIT);
        pb.redirectOutput(Redirect.INHERIT);
        Process p = pb.start();
        p.waitFor();
    }

    private static String removeArgPairOrNull(String arg, List<String> list) {
        if (list.contains(arg)) {
            int index = list.indexOf(arg);
            list.remove(index);
            return list.remove(index);
        }
        return null;
    }

    private static boolean removeArgSingle(String arg, List<String> list) {
        if (list.contains(arg)) {
            list.remove(list.indexOf(arg));
            return true;
        }
        return false;
    }

    private void gc() {
        for (int i = 0; i < 10; i++) {
            byte[] bytes = new byte[1024];
            Arrays.fill(bytes, (byte) 1);
            bytes = null;
            System.gc();
            System.runFinalization();
        }
    }

}
The Coordinator
  • 13,007
  • 11
  • 44
  • 73
  • Which code is complaining about the manifest? What does the exact error message look like? What does `findMainClass` do? – MvG Nov 20 '12 at 13:39
  • findMainClass just looks at the jar manifest to find the main class. I will repost the entire program. – The Coordinator Nov 22 '12 at 00:57
  • This fails after the urlclassloader is reloaded with a message that it cannot find the method (No SuchMethod): clazz.getMethod("reload") Maybe you can try it and see where the bug is, or maybe this is some limitation to disposing and re-loading a urlclassloader ?/ – The Coordinator Nov 22 '12 at 01:06
  • what's the _actual_ exception message? – jtahlborn Nov 22 '12 at 03:02
  • there's nothing stopping you from loading many URLClassloaders in the same jvm with the same jar, so the issue is somewhere else. are you replacing the jar while the program is running? – jtahlborn Nov 22 '12 at 03:06
  • After providing the class name manually, not using the manifest, it works really well. Why the manifest does not locate the second time befudles me! The bigger problem is that the URLClassloader does not let go of the jar file so it can be replaced. Windows reports that the files are still in use. – The Coordinator Nov 22 '12 at 08:23

1 Answers1

0

After debugging, I determined the original difficulty was that the Launcher was not isolating the UrlClassloader.

By putting the part of the program that starts the classloader in a seperate method, I was able to allow all the system to determine that no more references existed.

After reworking the code, it all works now :)

The Coordinator
  • 13,007
  • 11
  • 44
  • 73