Start Java process from Java

I was working on a project where I wanted to be able to run a Java process silently, similar to a windows service or a Unix daemon process, but platform independent.

I wanted to start a java executable giving it the same classpath, and class to run as the current JVM. I want it to use the same environment as the running JVM, and to use the same working directory. The principle used here is very similar to the Unix fork system call.

So to get the information, we can use this code:

String javaHome = System.getProperty("java.home");  
final String javaLauncher = new File(javaHome, "bin/java").getPath();  
final String classPath = System.getProperty("java.class.path");  
final String currentDirectory = System.getProperty("user.dir");  

The javaLauncher can always be called as java even though the executable in Windows is called java.exe, and it is always going to reside in the bin folder of the java installation directory.

The classpath is found in the system property java.class.path, and the current directory is found in system property user.dir.

You can list all the current system properties like this:

System.getProperties().list(System.out);  

And you can show the current environment of you process like this:

System.out.println(System.getenv());  

We need to know the name of the class we are currently running, so we can start it in the new process. The following method implementation will do that:

private static String getCurrentClassName() throws Exception {  
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    int index = stackTrace.length-2;
    StackTraceElement stackTraceElement = stackTrace[index];
    Class<?> c = Class.forName(stackTraceElement.getClassName());
    return c.getCanonicalName();
}

Notice how we take the second last stack trace element. In that way it will be the class that called the method. We could also just have hardcoded the class name, or take it from a configuration file.

The next thing to do is to be able to determine in the code whether we are in the process that should launch another process, or if we have been launched. If we didn't do that, the process would just launch itself over and over until it consumed all resources on the computer.

We do the separation by using an argument to the process called -launch. If this is the first argument, we launch the process with all but the first argument, otherwise we know that we have been launched, and can go on to do what the process was meant to do.

if(args.length > 0 && args[0].equals("-launch")) {  
    // Launch the process
} else { 
    FileWriter writer = new FileWriter(File.createTempFile("launch-", ".log", new File(currentDirectory)));
    writer.write("Launched");
    writer.close();
    Thread.sleep(10000);
}

The process just writes the word Launched to a file in the current directory and sleeps for 10 seconds to simulate a long running process. It gives us time to verify that we did indeed start another process.

Now let's look at how to actually start the process. We will be using the ProcessBuilder, since that is the recommended over using Runtime.exec().

First we will get all the arguments together:

final ArrayList<String> argumentList = new ArrayList<String>() {  
    {
        add(javaLauncher);
        add("-cp");
        add(classPath);
        add(currentClassName);
        for(int i= 1; i<processArgs.length; i++) {
            add(processArgs[i]);
        }
    }
};

You may not be familiar with this inline definition of the class, but it is simply to avoid having to type argumentList.add over, and over. For performance reasons, you may decide not to do it this way, since it results in an additional class file after compilation.

Next we define the thread that will start the process, like this:

Runnable processLauncher = new Runnable() {

    @Override
    public void run() {
        try {
            new ProcessBuilder(argumentList).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

};
new Thread(processLauncher).start();  

It turns out that starting a process like this will automatically use the same environment, and working directory as the current process, so we don't need to worry about that for our example.

If you do want to modify the environment, you need to retrieve the Map using ProcessBuilder.environment() , and then modify it.

You can also change the working directory by calling ProcessBuilder.directory(File directory).

If you are running the example on Windows, you can verify the creation of the new process using command tasklist, and on Unix variants, you can use ps.

Here is the complete example code:

package com._4thex.launcher;

import java.io.File;  
import java.io.FileWriter;  
import java.io.IOException;  
import java.util.ArrayList;

public class main {

    public static void main(String[] args) throws Exception {
        String javaHome = System.getProperty("java.home");
        final String javaLauncher = new File(javaHome, "bin/java").getPath();
        final String classPath = System.getProperty("java.class.path");
        final String currentDirectory = System.getProperty("user.dir");
        final String currentClassName = getCurrentClassName();
        final String[] processArgs = args;
        if(args.length > 0 && args[0].equals("-launch")) {
            final ArrayList<String> argumentList = new ArrayList<String>() {
                {
                    add(javaLauncher);
                    add("-cp");
                    add(classPath);
                    add(currentClassName);
                    for(int i= 1; i<processArgs.length; i++) {
                        add(processArgs[i]);
                    }
                }
            };
            Runnable processLauncher = new Runnable() {

                @Override
                public void run() {
                    try {
                        new ProcessBuilder(argumentList).start();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            };
            new Thread(processLauncher).start();
            System.out.println("Launcher completed");
        } else { 
            FileWriter writer = new FileWriter(File.createTempFile("launch-", ".log", new File(currentDirectory)));
            writer.write("Launched");
            writer.close();
            Thread.sleep(10000);
        }
    }

    private static String getCurrentClassName() throws Exception {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        int index = stackTrace.length-2;
        StackTraceElement stackTraceElement = stackTrace[index];
        Class<?> c = Class.forName(stackTraceElement.getClassName());
        return c.getCanonicalName();
    }

}