1. 概述
在这篇文章中,我们将学习如何从Java应用程序执行shell命令。
首先,我们将使用Runtime
类提供的*exec()*
方法。然后,我们将学习ProcessBuilder
,它提供了更多的自定义选项。
2. 操作系统依赖性
shell命令是依赖于操作系统的,因为它们在不同系统上的行为会有所不同。因此,在创建任何运行shell命令的Process
之前,我们需要知道JVM运行的操作系统。
此外,在Windows上,shell通常被称为cmd.exe
。而在Linux和macOS上,shell命令是通过/bin/sh
来运行的。为了在这些不同的机器上保持兼容,我们可以程序性地在Windows机器上添加cmd.exe
,否则添加/bin/*sh
。例如,我们可以通过读取System
类的"os.name"
属性来判断代码运行的机器是否为Windows:
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");
3. 输入和输出
通常,我们需要连接进程的输入和输出流。具体来说,InputStream
作为标准输入,OutputStream
作为标准输出。我们必须始终消费输出流,否则进程将不会返回并永远挂起。
让我们实现一个常用的类StreamGobbler
,它消费InputStream
:
private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;
public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}
这个类实现了Runnable
接口,这意味着任何Executor
都可以执行它。
4. Runtime.exec()
接下来,我们将使用.exec()
方法启动一个新的进程,并使用之前创建的StreamGobbler
。
例如,我们可以列出用户主目录中的所有子目录,然后将其打印到控制台:
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("/bin/sh -c ls %s", homeDirectory));
}
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Future<?> future = executorService.submit(streamGobbler);
int exitCode = process.waitFor();
assertDoesNotThrow(() -> future.get(10, TimeUnit.SECONDS));
assertEquals(0, exitCode);
这里,我们使用.newSingleThreadExecutor()
创建了一个新的子进程,然后使用.submit()
运行包含shell命令的Process
。此外,.submit()
返回一个*Future*
对象,我们可以利用它来检查进程的结果。同时,确保调用返回对象的.get()
方法来等待计算完成。如果你是从main方法中运行上述代码,请确保对executorService对象调用shutdown,否则代码将永远不会停止。这同样适用于下面的所有示例。在我们的代码中,我们使用JUnit生命周期方法来进行必要的清理,如那样做。
注意:JDK 18已弃用Runtime
类的*.exec(String command)
方法。
4.1. 处理管道
目前,无法使用.exec()
处理管道。幸运的是,管道是shell特性的一部分。因此,我们可以在需要使用管道的地方创建整个命令,然后传递给.exec()
:
if (IS_WINDOWS) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s | findstr \"Desktop\"", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("/bin/sh -c ls %s | grep \"Desktop\"", homeDirectory));
}
在这里,我们列出了用户主目录中的所有目录,并搜索“Desktop”文件夹。
5. ProcessBuilder
另一种选择是使用*ProcessBuilder*
,它比Runtime
方法更受青睐,因为我们可以定制它,而不仅仅是运行一个字符串命令。
简而言之,通过这种方法,我们可以:
- 通过
.directory()
更改shell命令运行的工作目录 - 通过提供键值对映射给
.environment()
来更改环境变量 - 以自定义方式重定向输入和输出流
- 将它们都继承给当前JVM进程的流,使用
.inheritIO()
同样,我们可以运行与前一个示例相同的shell命令:
ProcessBuilder builder = new ProcessBuilder();
if (isWindows) {
builder.command("cmd.exe", "/c", "dir");
} else {
builder.command("sh", "-c", "ls");
}
builder.directory(new File(System.getProperty("user.home")));
Process process = builder.start();
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Future<?> future = executorService.submit(streamGobbler);
int exitCode = process.waitFor();
assertDoesNotThrow(() -> future.get(10, TimeUnit.SECONDS));
assertEquals(0, exitCode);
5.1. 处理管道
Java 9引入了ProcessBuilder
API中的管道概念:
public static List<Process> startPipeline(List<ProcessBuilder> builders) throws IOException
使用*startPipeline*
方法,我们可以传递一个ProcessBuilder
对象列表。此静态方法将为每个ProcessBuilder
启动一个Process
。这样就创建了一个由标准输入和输出流链接的进程链。
例如,我们可以为每个独立的命令创建一个进程构建器,并将它们组合成管道:
@Test
public void givenProcessBuilder_whenStartingPipeline_thenSuccess()
throws IOException, InterruptedException {
List<ProcessBuilder> builders = Arrays.asList(
new ProcessBuilder("find", "src", "-name", "*.java", "-type", "f"),
new ProcessBuilder("wc", "-l"));
List<Process> processes = ProcessBuilder.startPipeline(builders);
Process last = processes.get(processes.size() - 1);
List<String> output = readOutput(last.getInputStream());
assertThat("Results should not be empty", output, is(not(empty())));
}
在上述示例中,我们在src
目录中搜索所有.java文件,并将结果管道到另一个进程进行计数。
要了解Java 9对Process
API的其他改进,请查看我们的文章《Java 9 Process API改进》:链接。
6. 总结
正如我们在快速教程中看到的,我们可以通过两种不同的方式在Java中执行shell命令。
一般来说,如果我们计划定制新启动进程的执行,例如改变其工作目录,应该考虑使用ProcessBuilder
。
如往常一样,源代码可以在GitHub上找到:链接。