ProcessBuilder和Runtime.exec()的区别

111

我正在尝试从Java代码中执行外部命令,但我注意到在使用 Runtime.getRuntime().exec(...)new ProcessBuilder(...).start() 时存在差异。

当使用 Runtime 时:

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

退出值为0,命令正常终止。

然而,使用ProcessBuilder

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

退出值为1001并且命令在中途终止,尽管 waitFor 返回。

我该如何修复 ProcessBuilder 的问题?

4个回答

112
Runtime.getRuntime().exec()方法有多种重载形式,可以接受一个字符串数组或单个字符串参数。使用单个字符串参数的exec()方法会将该字符串拆分成若干个参数,然后将这些参数作为字符串数组传递给另一个接受字符串数组的exec()重载方法。而ProcessBuilder构造函数只接受一个可变长度的字符串数组或一个字符串列表,其中数组或列表中的每个字符串都被视为一个独立的参数。无论哪种方式,最终得到的参数都会被组合成一个字符串并传递给操作系统执行。
例如,在Windows系统上:
Runtime.getRuntime().exec("C:\DoStuff.exe -arg1 -arg2");

这个程序将使用给定的两个参数运行DoStuff.exe程序。在这种情况下,命令行被分词并重新组合。

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

如果不巧在C:\中存在一个名为DoStuff.exe -arg1 -arg2的程序,否则将会失败。这是因为没有进行记号化(tokenisation):运行命令被假定已经被分成了单独的单词(或记号)。相反,你应该使用

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

或者,另一种选择是

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

仍然无法工作:List<String> params = java.util.Arrays.asList(installation_path+uninstall_path+uninstall_command, uninstall_arguments); Process qq=new ProcessBuilder(params).start(); - gal
7
我无法相信这个字符串连接有任何意义:"installation_path+uninstall_path+uninstall_command"。 - Angel O'Sphere
9
除非在命令中显示指定了shell,否则Runtime.getRuntime().exec(...)不会调用shell。这对于最近的“Shellshock”漏洞问题是一件好事。这个答案有误导性,因为它声称将运行cmd.exe或等效的Unix shell(即/bin/bash),但似乎并非如此。相反,Java环境内部进行了标记化处理。 - Stefan Paul Noack
@noah1989:感谢反馈。我已更新我的回答(希望)澄清了一些事情,特别是删除了任何关于shell或cmd.exe的提及。 - Luke Woodward
执行器的解析器与参数化版本并不完全相同,这让我花了几天时间才弄清楚... - Drew Delano
@DrewDelano - 是的。 "解析器" 实际上就是 String.split("\\s+") :-) - Stephen C

21

ProcessBuilder.start()Runtime.exec()之间没有区别,因为Runtime.exec()的实现方式是:

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();
}

所以代码:

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();

应该与以下内容相同:

Runtime.exec(command)

感谢dave_thompson_085的评论


2
但是Q不调用那个方法。它(间接地)调用public Process exec(String command, String[] envp, File dir) -- String而不是String[] -- 这将调用StringTokenizer并将标记放入数组中,然后将该数组(间接地)传递给ProcessBuilder,这是正确陈述的三个答案中的一个区别,距今已有7年之久。 - dave_thompson_085
无论问题有多久,我都会尽力修复答案。 - Eugene Lopatkin
我无法为ProcessBuilder设置环境,只能获取环境... - ilke Muhtaroglu
请参考 https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html#inheritIO(),在通过环境方法获取环境后设置环境。 - ilke Muhtaroglu
如果您仔细查看,您会发现环境默认为空。 - Eugene Lopatkin
有三个重载的方法 exec - Eugene Lopatkin

20

观察Runtime.getRuntime().exec()如何将字符串命令传递给ProcessBuilder。它使用一个分词器并将命令拆分成单个标记,然后调用exec(String[] cmdarray, ......)构造一个ProcessBuilder

如果你使用字符串数组构造ProcessBuilder,则会得到与单个字符串相同的结果。

ProcessBuilder构造函数采用String...可变参数,因此将整个命令作为单个字符串传递具有与在终端中用引号调用该命令相同的效果:

shell$ "command with args"

19
是的,存在差异。
  • Runtime.exec(String) 方法 接受一个字符串作为指令,将其分解成命令和一系列参数。

  • ProcessBuilder 构造器 接受一个(可变长度)字符串数组。第一个字符串是指令名称,其余部分是参数。(虽然也有一个接受字符串列表的构造器,但没有一个接受单个包含命令和参数的字符串。)

因此,您要让 ProcessBuilder 执行的是一个包含空格和其他垃圾字符的“命令”名称。当然,操作系统找不到该名称的命令,因此命令执行失败。

不,它们没有区别。Runtime.exec(String)是ProcessBuilder的快捷方式。还支持其他构造函数。 - marcolopes
2
你是错误的。阅读源代码!Runtime.exec(cmd)实际上是Runtime.exec(cmd.split("\\s+"))的快捷方式。ProcessBuilder类没有一个直接等价于Runtime.exec(cmd)的构造函数。这就是我在回答中要表达的观点。 - Stephen C
1
实际上,如果您像这样实例化一个ProcessBuilder:new ProcessBuilder("command arg1 arg2"),则start()调用将不会执行您期望的操作。它可能会失败,并且仅在您的命令名称中有空格时才能成功。这正是OP正在询问的问题! - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接