ProcessBuilder 与 Runtime.exec()的区别

我试图从 Java 代码执行一个外部命令,但是我注意到 Runtime.getRuntime().exec(...)new ProcessBuilder(...).start()之间有一个区别。

使用 Runtime时:

Process p = Runtime.getRuntime().exec(installation_path +
uninstall_path +
uninstall_command +
uninstall_arguments);
p.waitFor();

ExitValue 为0,命令终止 OK。

然而,对于 ProcessBuilder:

Process p = (new ProcessBuilder(installation_path +
uninstall_path +
uninstall_command,
uninstall_arguments)).start();
p.waitFor();

出口值为1001,命令在中间终止,尽管 waitFor返回。

我应该做什么来解决 ProcessBuilder的问题?

123378 次浏览

Look at how Runtime.getRuntime().exec() passes the String command to the ProcessBuilder. It uses a tokenizer and explodes the command into individual tokens, then invokes exec(String[] cmdarray, ......) which constructs a ProcessBuilder.

If you construct the ProcessBuilder with an array of strings instead of a single one, you'll get to the same result.

The ProcessBuilder constructor takes a String... vararg, so passing the whole command as a single String has the same effect as invoking that command in quotes in a terminal:

shell$ "command with args"

Yes there is a difference.

  • The Runtime.exec(String) method takes a single command string that it splits into a command and a sequence of arguments.

  • The ProcessBuilder constructor takes a (varargs) array of strings. The first string is the command name and the rest of them are the arguments. (There is an alternative constructor that takes a list of strings, but none that takes a single string consisting of the command and arguments.)

So what you are telling ProcessBuilder to do is to execute a "command" whose name has spaces and other junk in it. Of course, the operating system can't find a command with that name, and the command execution fails.

The various overloads of Runtime.getRuntime().exec(...) take either an array of strings or a single string. The single-string overloads of exec() will tokenise the string into an array of arguments, before passing the string array onto one of the exec() overloads that takes a string array. The ProcessBuilder constructors, on the other hand, only take a varargs array of strings or a List of strings, where each string in the array or list is assumed to be an individual argument. Either way, the arguments obtained are then joined up into a string that is passed to the OS to execute.

So, for example, on Windows,

Runtime.getRuntime().exec("C:\DoStuff.exe -arg1 -arg2");

will run a DoStuff.exe program with the two given arguments. In this case, the command-line gets tokenised and put back together. However,

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe -arg1 -arg2");

will fail, unless there happens to be a program whose name is DoStuff.exe -arg1 -arg2 in C:\. This is because there's no tokenisation: the command to run is assumed to have already been tokenised. Instead, you should use

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe", "-arg1", "-arg2");

or alternatively

List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);

There are no difference between ProcessBuilder.start() and Runtime.exec() because implementation of Runtime.exec() is:

public Process exec(String command) throws IOException {
return exec(command, null, null);
}


public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");


StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}


public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

So code:

List<String> list = new ArrayList<>();
new StringTokenizer(command)
.asIterator()
.forEachRemaining(str -> list.add((String) str));
new ProcessBuilder(String[])list.toArray())
.environment(envp)
.directory(dir)
.start();

should be the same as:

Runtime.exec(command)

Thanks dave_thompson_085 for comment