54

Can a Java application be loaded in a separate process using its name, as opposed to its location, in a platform independent manner?

I know you can execute a program via ...

Process process = Runtime.getRuntime().exec( COMMAND );

... the main issue of this method is that such calls are then platform specific.


Ideally, I'd wrap a method into something as simple as...

EXECUTE.application( CLASS_TO_BE_EXECUTED );

... and pass in the fully qualified name of an application class as CLASS_TO_BE_EXECUTED.

tshepang
  • 12,111
  • 21
  • 91
  • 136
Ande Turner
  • 7,096
  • 19
  • 80
  • 107
  • 1
    So if I get you right, you have several classes with main() methods and you want to launch them in separate processes? – Michael Myers Mar 11 '09 at 21:32
  • How about if you exec("java.exe", CLASS_TO_BE_EXECUTED.class.getName()) ? – Michael Myers Mar 11 '09 at 21:49
  • how to take input from a user for java class running as a process started itself by a java program, using something like br.readLine() – paragjain Nov 15 '10 at 18:31
  • 1
    I'm with the OP, it would be nice if we could bypass the whole CLI interface. Someone really should come up with a wrapper class to do that so that application developers can focus on business logic. – Sridhar Sarnobat Mar 03 '15 at 00:11

9 Answers9

83

This is a synthesis of some of the other answers that have been provided. The Java system properties provide enough information to come up with the path to the java command and the classpath in what, I think, is a platform independent way.

public final class JavaProcess {

    private JavaProcess() {}        

    public static int exec(Class klass, List<String> args) throws IOException,
                                               InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome +
                File.separator + "bin" +
                File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = klass.getName();

        List<String> command = new LinkedList<String>();
        command.add(javaBin);
        command.add("-cp");
        command.add(classpath);
        command.add(className);
        if (args != null) {
            command.addAll(args);
        }

        ProcessBuilder builder = new ProcessBuilder(command);

        Process process = builder.inheritIO().start();
        process.waitFor();
        return process.exitValue();
    }

}

You would run this method like so:

int status = JavaProcess.exec(MyClass.class, args);

I thought it made sense to pass in the actual class rather than the String representation of the name since the class has to be in the classpath anyways for this to work.

Eduardo
  • 319
  • 2
  • 14
hallidave
  • 9,579
  • 6
  • 31
  • 27
  • 4
    You might need to use `redirectOutput(...)` and `redirectError(...)` on the process builder (before `start()`) if you want to start some kind of server, otherwise it might hang and you might not be able to contact it. Not sure why though... – nyg Apr 12 '16 at 11:47
  • How to run the new process in detached mode, so that the outer process can continue its work – daydreamer Jun 05 '16 at 00:20
  • Thanks! Separating out "-cp" from the actual classpath helped. When making them together as a single String, it errors out "Unrecognized option: -cp " – asgs Oct 25 '16 at 11:29
  • 1
    It might be better to use `String className = klass.getName();` instead of `String className = klass.getCanonicalName();` to properly handle nested classes. – John29 Nov 19 '18 at 21:22
  • does anyone know the technique to include VM arguments i.e -D and -X for ProcessBuilder ? I've followed the instructions on the api doc to use processbuilder.environment() to see a Map but the values don't appear to be available in the called programme - i.e System.getProperty("xyz", value) is not available in the called programme even though I can see its being set correctly – robbie70 Apr 09 '19 at 17:34
47

Two hints:

System.getProperty("java.home") + "/bin/java" gives you a path to the java executable.

((URLClassLoader) Thread.currentThread().getContextClassLoader()).getURL() helps you to reconstruct the classpath of current application.

Then your EXECUTE.application is just (pseudocode):

Process.exec(javaExecutable, "-classpath", urls.join(":"), CLASS_TO_BE_EXECUTED)

RamenChef
  • 5,557
  • 11
  • 31
  • 43
stepancheg
  • 4,262
  • 2
  • 33
  • 38
5

Expanding on @stepancheg's answer the actual code would look like so (in the form of a test).

import org.junit.Test;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.stream.Collectors;

public class SpinningUpAJvmTest {
    @Test
    public void shouldRunAJvm() throws Exception {
        String classpath = Arrays.stream(((URLClassLoader) Thread.currentThread().getContextClassLoader()).getURLs())
                .map(URL::getFile)
                .collect(Collectors.joining(File.pathSeparator));
        Process process = new ProcessBuilder(
                System.getProperty("java.home") + "/bin/java",
                "-classpath",
                classpath,
                MyMainClass.class.getName()
                // main class arguments go here
        )
                .inheritIO()
                .start();
        int exitCode = process.waitFor();
        System.out.println("process stopped with exitCode " + exitCode);
    }
}
kairius
  • 395
  • 6
  • 10
andrej
  • 176
  • 2
  • 3
5

This might be an overkill for you, but Project Akuma does what you want and more. I found it via this entry at Kohsuke's (one of Sun's rock start programmers) fabulously useful blog.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 1
    Looks like it was moved to Github. Makes sense, given how sour Oracle got with the whole Jenkins/Hudson thing. – StaxMan Jun 05 '13 at 01:11
4
public abstract class EXECUTE {

    private EXECUTE() { /* Procedural Abstract */ }

    public static Process application( final String CLASS_TO_BE_EXECUTED ) {

        final String EXEC_ARGUMENT 
        = new StringBuilder().
              append( java.lang.System.getProperty( "java.home" ) ).
              append( java.io.File.separator ).
              append( "bin" ).
              append( java.io.File.separator ).
              append( "java" ).
              append( " " ).
              append( new java.io.File( "." ).getAbsolutePath() ).
              append( java.io.File.separator ).
              append( CLASS_TO_BE_EXECUTED ).
              toString();

        try {       

            return Runtime.getRuntime().exec( EXEC_ARGUMENT );

        } catch ( final Exception EXCEPTION ) {     

            System.err.println( EXCEPTION.getStackTrace() );
        }

        return null;
    }
}
Ande Turner
  • 7,096
  • 19
  • 80
  • 107
4

Did you check out the ProcessBuilder API? It's available since 1.5

http://java.sun.com/javase/6/docs/api/java/lang/ProcessBuilder.html

lothar
  • 19,853
  • 5
  • 45
  • 59
3

Do you really have to launch them natively? Could you just call their "main" methods directly? The only special thing about main is that the VM launcher calls it, nothing stops you from calling main yourself.

TofuBeer
  • 60,850
  • 18
  • 118
  • 163
2

Following on what TofuBeer had to say: Are you sure you really need to fork off another JVM? The JVM has really good support for concurrency these days, so you can get a lot of functionality for relatively cheap by just spinning off a new Thread or two (that may or may not require calling into Foo#main(String[])). Check out java.util.concurrent for more info.

If you decide to fork, you set yourself up for a bit of complexity related to finding required resources. That is, if your app is changing frequently and depends upon a bunch of jar files, you'll need to keep track of them all so that they can be passed out to the classpath arg. Additionally, such an approach requires to to infer both the location of the (currently executing) JVM (which may not be accurate) and the location of the current classpath (which is even less likely to be accurate, depending upon the way that the spawning Thread has been invoked - jar, jnlp, exploded .classes dir, some container, etc.).

On the other hand, linking into static #main methods has its pitfalls as well. static modifiers have a nasty tendency of leaking into other code and are generally frowned upon by design-minded folks.

jasonnerothin
  • 1,556
  • 10
  • 15
  • RMI will subsequently be involved, the processes act as Daemons for the main "Kernel" process, and there is some chance they may not be on the same computer but somewhere else on the network. I thought the original phrasing is useful to more readers though. – Ande Turner Apr 07 '09 at 00:52
1

A problem that occurs when you run this from a java GUI is it runs in the background. So you cannot see the command prompt at all.

To get around this, you have to run the java.exe through "cmd.exe" AND "start". I dont know why, but if you put "cmd /c start" infront it shows the command prompt as it runs.

However, the problem with "start" is that if there is a space in the path to the application (which the path to the java exe usually have as it is in C:\Program Files\Java\jre6\bin\java.exe or similar), then start just fails with "cannot find c:\Program"

So you have to put quotes around C:\Program Files\Java\jre6\bin\java.exe Now start complains about parameters that you pass to java.exe: "The system cannot find the file -cp."

Escaping the space in "Program Files" with a backslash also does not work. So the idea is to not use space. Generate a temporary file with the bat extension and then put your command with spaces in there and run the bat. However, running a bat through start, does not exit when done, so you have to put "exit" at the end of the batch file.

This still seems yucky.

So, looking for alternatives, I have found that using quote space quote in the space of "Program Files" actually works with start.

In the EXECUTE class above change the string builder appends to:

append( "cmd /C start \"Some title\" " ).
append( java.lang.System.getProperty( "java.home" ).replaceAll(" ", "\" \"") ).
append( java.io.File.separator ).
append( "bin" ).
append( java.io.File.separator ).
append( "java" ).
append( " " ).
append( new java.io.File( "." ).getAbsolutePath() ).
append( java.io.File.separator ).
append( CLASS_TO_BE_EXECUTED ).
Matiaan
  • 11
  • 1