8

What i am trying to do is writing a self-updating Spring Boot 2.2.5 application

I wrote a little spring-boot-starter-web application with a RESTController to control the update process (i know the REST-Paths are not properly set e.g. /update/update...)

The process should look like the following:

  1. Start the Spring-Boot application with self delivered AdoptOpenJDK from the application-libraries
  2. Check if update is available on Server (http://localhost:8080/update/check)
  3. Download the update to a local directory and unzip all files into a temporary "update-directory. (http://localhost:8080/update/download)
  4. Shutdown Spring-Boot application including the JVM (http://localhost:8080/update/update)
  5. Modify application folders (replace libraries and other files)
  6. Start the application the same way it was started in point 1 (With self delivered AdoptOpenJDK and params)

For now i tried accomplishing this task by using the following tutorial:

https://dzone.com/articles/programmatically-restart-java

With this example i can restart my application just fine. But i have to care for the OS specific Shell/Commands to work properly. e.g. i can't get it running on Windows together with a new CMD-Window. Only if the application is started in background i dont get any errors, or at least the application is starting and responding.

I had a look at the Spring Boot Actuator stuff aswell, but this is mostly reloading the context, but i need to swap the ressources used by the JVM currently running..

So what i want to ask: Is there a way to restart my Spring-Boot-Application including parameters, updating all the files from the application and restarting the JVM?

My current code is the following:

SelfupdateApplication (Spring-Boot-Start-Class)

@SpringBootApplication
public class SelfupdateApplication {

    public static void main(String[] args) {
        SpringApplication.run(SelfupdateApplication.class, args);
    }

    public static void update() {
        try {
            SelfupdateApplication.restartApplication(null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Sun property pointing the main class and its arguments. Might not be defined
     * on non Hotspot VM implementations.
     */
    public static final String SUN_JAVA_COMMAND = "sun.java.command";

    /**
     * Restart the current Java application
     * 
     * @param runBeforeRestart some custom code to be run before restarting
     * @throws IOException
     */
    public static void restartApplication(Runnable runBeforeRestart) throws IOException {
        try {
            // java binary
            String java = System.getProperty("java.home") + "/bin/java";
            System.out.println("Java-Home: " + java);
            // vm arguments
            List<String> vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
            StringBuffer vmArgsOneLine = new StringBuffer();
            for (String arg : vmArguments) {
                // if it's the agent argument : we ignore it otherwise the
                // address of the old application and the new one will be in conflict
                if (!arg.contains("-agentlib")) {
                    vmArgsOneLine.append(arg);
                    vmArgsOneLine.append(" ");
                }
            }
            // init the command to execute, add the vm args
            final StringBuffer cmd = new StringBuffer("\"" + java + "\" " + vmArgsOneLine);

            // program main and program arguments
            String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" ");
            // program main is a jar
            if (mainCommand[0].endsWith(".jar")) {
                // if it's a jar, add -jar mainJar
                cmd.append("-jar " + new File(mainCommand[0]).getPath());
            } else {
                // else it's a .class, add the classpath and mainClass
                cmd.append("-cp \"" + System.getProperty("java.class.path") + "\" " + mainCommand[0]);
            }
            // finally add program arguments
            for (int i = 1; i < mainCommand.length; i++) {
                cmd.append(" ");
                cmd.append(mainCommand[i]);
            }
            // execute the command in a shutdown hook, to be sure that all the
            // resources have been disposed before restarting the application
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    try {
                        Runtime.getRuntime().exec(cmd.toString());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            // execute some custom code before restarting
            if (runBeforeRestart != null) {
                runBeforeRestart.run();
            }
            // exit
            System.exit(0);
        } catch (Exception e) {
            // something went wrong
            throw new IOException("Error while trying to restart the application", e);
        }
    }
}

RestController to call the single steps

@RestController
@RequestMapping("/update")
public class UpdateController {

    @GetMapping("/download")
    public boolean download() {
        File currentDir = new File(System.getProperty("user.dir"));
        File destDir = new File(currentDir.getAbsolutePath() + File.separator + "update_lib");
        File downloadDir = new File("C:/temp/selfupdate/someServer/update");

        try {
            FileUtils.copyDirectory(downloadDir, destDir);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    @GetMapping("/check")
    public void check() {
        // not relevant for now
        SomeHelper.checkForUpdate();
    }

    @GetMapping("/update")
    public void update() {
        SelfupdateApplication.update();
    }

    @GetMapping("/restart")
    public void restart() {
        try {
            SelfupdateApplication.restartApplication(null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @GetMapping("/alive")
    public String alive() {
        return "Yes i am still here ;-)";
    }

    @GetMapping("/shutdown")
    public String shutdown() {
        Thread thread = new Thread() {
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.exit(0);
            }
        };
        thread.start();
        return "System shutdown initiated";
    }

    @GetMapping("/version")
    public String version() {
        return "0.0.1";
    }

}

Thanks for reading and i appreciate your suggestions!

marc overath
  • 81
  • 1
  • 3

1 Answers1

0

For the ones that want to understand the process.

This code runs a spring boot application jar from another application jar using reflection to be platform independently.

demo-0.0.1-SNAPSHOT.jar it's a simple spring boot app that I'm not including here to do not make this post too long.

The main method calls other 3 methods

  • checkForUpdate - here you must include your own logic to check for update
  • updateJar - this method download your application jar file from a remote server
  • runjar - This one uses reflection to execute your application

It's necessary two apps, one for update the another one to don't bock the file that you want to overwrite.

public class Main {

    private static final String remoteJarFile = "https://www.example.org/demo-0.0.1-SNAPSHOT.jar";

    private static final File jarFile = new File("/home/example/demo-0.0.1-SNAPSHOT.jar");

    public static void main(String[] args) throws Exception {
        Main main = new Main();

        if (main.checkForUpdate())
            main.updateJar();

        main.runJar();
    }

    private void runJar() throws Exception {
        URLClassLoader classLoader = new URLClassLoader(
                new URL[] {jarFile.toURI().toURL()},
                this.getClass().getClassLoader());

        Class mainClass = Class.forName("org.springframework.boot.loader.JarLauncher", true, classLoader);

        Method method = mainClass.getDeclaredMethod("main", String[].class);

        method.invoke(null, (Object) new String[0]);
    }

    public boolean checkForUpdate() {
        return true;
    }

    public void updateJar() throws Exception {
        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(jarFile));

        BufferedInputStream inputStream = new BufferedInputStream(new URL(remoteJarFile).openStream());

        try {
            while (inputStream.available() > 0)
                outputStream.write(inputStream.read());
        } finally {
            inputStream.close();
            outputStream.close();
        }
    }
}
btafarelo
  • 601
  • 3
  • 12