前言
Java 程序中,如果我们想执行一些 Shell 命令或其他外部应用程序,通常都是使用java.lang.Runtime.exec(..)
方法来执行的。
当 Java 内置的 Runtime.exec(..)
方法在执行外部命令时,可能存在一些不易察觉的坑,往往会导致程序运行失败。
在网上看到一篇文章:『When Runtime.exec() won't』,里面对Runtime.exec(..)
调用可能出现的坑进行了讲解,我觉得讲的很好,就记录一下。
简介
在简析Runtime.exec(..)
可能出现的坑之前,先了解下该 API 的一些信息。
首先查看一下类Runtime
的文档,如下图所示:
每一个 Java 应用程序都拥有唯一的
Runtime
实例,使得该应用程序可以与其运行中的环境进行交互。该单例Runtime
实例可以通过Runtime.getRuntime()
进行获取。Java 应用程序不能手动创建属于自己的一个
Runtime
实例。
简而言之,每个 Java 程序都拥有唯一一个Runtime
实例,而Runtime
实例的功能就是可以与运行时环境进行交互。
注:所谓的运行时环境,个人理解,指的就是当前运行该 Java 程序(或更确切的说,当前运行的 JVM 进程)所在的环境,通常指的就是运行该程序的操作系统。
下面来看下Runtime.exe(..)
方法,如下图所示:
从官方文档可以看到,Runtime.exec(..)
方法共有 6 个重载方法,其源码如下:
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, null);
}
public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}
public Process exec(String command, String[] envp, File dir)
throws IOException {
...
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();
}
所有的exec
方法最终调用的都是exec(String[] cmdarray, String[] envp, File dir)
,所以看下该方法文档,如下图所示:
Runtimie.exec(..)
会在给定的环境和工作目录下启动一个独立的进程运行外部命令。其中,其参数含义如下:
-
cmdarray
:字符串数组,包含要运行的命令和其参数。 -
envp
:字符串数组,每个元素以name=value
格式为子进程设置环境变量,为null
则表示子进程继承当前进程的运行时环境。 -
dir
:子进程的工作目录,为null
则表示继承当前进程的工作目录。
举个栗子:
- 执行命令:
dir D:
,代码如下:
Runtime.getRuntime().exec("cmd /c dir D:");
- 为新启动的子进程设置环境变量(当外部应用内部需要某个环境变量设置后才能正常运行时),比如,设置环境变量
name=vavlue
:
Runtime.getRuntime().exec("someApplication", new String[]{"name=value"});
- 执行 D:\test.exe,代码如下:
Runtime.getRuntime().exec("D:\\test.exe");
// 或者通过设置子进程工作目录
Runtime.getRuntime().exec("test.exe", null, new File("D:\\"));
注:以上栗子无论执行成功或失败,均不显示输出。为了捕获子进程运行结果输出,请参考后文内容。
下面列举『When Runtime.exec() won't』该文章提及的Runtime.exe(..)
方法可能出现的陷阱。
陷阱
-
IllegalThreadStateException
异常:Runtime.exe(..)
可能出现IllegalThreadStateException
异常。我们通常使用exec(..)
方法执行 JVM 外部程序,如果想查看外部程序返回值,可以使用Process.exitValue()
方法。需要注意的是,如果直接使用
Process.exitValue()
获取外部程序返回值,如果此时外部程序还未运行完成,则会抛出IllegalThreadStateException
异常。举个栗子:比如在 Java 中执行 javac 程序,并获取其返回值。
代码如下:public class BadExecJavac { public static void main(String[] args) throws IOException { Process pid = Runtime.getRuntime().exec("javac"); int exitValue = pid.exitValue(); System.out.println("Process exitValue: " + exitVal); } }
结果如下:
解决方案:使用
Process.waitFor()
替换Process.exitValue()
。
Process.waitFor()
与Process.exitValue()
同样会返回外部程序的执行结果,但是它会阻塞直到外部程序运行结束。
代码如下:public static void main(String[] args) throws IOException, InterruptedException { Process pid = Runtime.getRuntime().exec("javac"); int exitValue = pid.waitFor(); System.out.println("Process exitValue: " + exitValue); }
结果如下:
注:
Process.exitValue()
/Process.waitFor()
获取外部程序的返回值为 0 表示执行成功,其余值表示外部程序执行出错。注:通常情况下,要获取外部程序返回值,一般都是使用
Process.waitFor()
。唯一一种可能使用Process.exitValue()
的情况就是外部程序一直处于运行状态,此时为了不阻塞当前 JVM 进程,就得使用Process.exitValue()
(需要手动进行IllegalThreadStateException
异常捕获)。综上:要解决
IllegalThreadStateException
异常,要么就是手动捕获Process.exitValue()
抛出的异常,要么就使用Process.waitFor()
(推荐)等待外部程序正常运行结束。 Runtime.exec(..)
处于悬挂状态:当运行外部程序时,外部程序可能处于阻塞状态,未能正常运行。
JDK 文档已对这种现象进行了描述:
Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.
外部程序处于悬挂状态的原因就在于:有些平台对标准输入/输出流只提供了有限的缓冲区,如果未能及时对外部程序进行输入流写入或者输出流读取,则可能导致外部程序被阻塞,甚至处于死锁状态。
所以解决方案就是对外部程序的输入或输出流进行处理。
需要注意的是:
- 对于外部程序来说,如果其需要进行输入,则我们要处理其输入流。而如果外部程序执行过程中有输出,那我们就要读取其输出流,清空其缓冲区。
- 对于应用程序来说,其输出流有两种:标准输出流(standard output stream) 和 标准错误流(standard error stream)。因此,一个健壮的代码应同时对这两种输出流进行处理。
我们继续完善前面的执行 javac 例子:为其添加读取外部程序输出流的功能。
代码如下:
andlerExecJavac {
public static void main(String[] args) throws IOException, InterruptedException {
Process pid = Runtime.getRuntime().exec("javac");
// 获取外部程序标准输出流
new Thread(new OutputHandlerRunnable(pid.getInputStream())).start();
// 获取外部程序标准错误流
new Thread(new OutputHandlerRunnable(pid.getErrorStream())).start();
int exitValue = pid.waitFor();
System.out.println("Process exitValue: " + exitValue);
}
private static class OutputHandlerRunnable implements Runnable {
private InputStream in;
public OutputHandlerRunnable(InputStream in) {
this.in = in;
}
@Override
public void run() {
try (BufferedReader bufr = new BufferedReader(new InputStreamReader(this.in))) {
String line = null;
while ((line = bufr.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里我们采用了两条线程分别处理外部程序的标准输出流和标准错误流,此处的输出流处理操作就是简单地输出到控制台上。
注:获取外部程序的输出流,我们代码中使用的是Process.getInputStream()
,因为在 Java 程序来看,外部程序的输出流,就是 Java 程序的输入流。同理,对于外部程序的输入流,就是 Java 程序的输出流。
-
Shell 命令并不一定是可执行文件:当我们使用
Runtime.exe(..)
执行一些 Shell 命令(如dir
/copy
)时,可能会出现找不到可执行文件错误。这是因为这些 Shell 命令是属于命令行解释器的一部分,并不是可执行文件。因此要运行这些命令,需要带上命令行解释器,如下所示:
// for windows
Runtime.getRuntime().exec("cmd /c dir");
// for linux
Runtime.getRuntime().exec("/bin/bash ls");
-
Runtime.exec()
不是一个命令行工具:相对于系统命令行工具,Runtime.exec(..)
会更加局限并且不具备跨平台功能,命令行能接收的任意字符串,Runtime.exec(..)
并不一定能接收。这个陷阱通常出现在Runtime.exec(String)
这个接收单个字符串执行外部命令的方法中。造成这个混乱的原因之一在于Runtime.exec(String)
将传递的字符串作为外部命令的参数,而命令行中可以区分一个字符串中的多个命令。举个栗子:执行命令
java Test$jecho > test.txt
,代码如下:public class CommandLineExec { public static void main(String[] args) throws IOException, InterruptedException { Process pid = Runtime.getRuntime().exec("java Test$jecho > test.txt"); new Thread(new OutputHandlerRunnable(pid.getInputStream())).start(); new Thread(new OutputHandlerRunnable(pid.getErrorStream())).start(); int exitValue = pid.waitFor(); System.out.println("Process exitValue: " + exitValue); } private static class OutputHandlerRunnable implements Runnable { private InputStream in; public OutputHandlerRunnable(InputStream in) { this.in = in; } @Override public void run() { try (BufferedReader bufr = new BufferedReader(new InputStreamReader(this.in))) { String line = null; while ((line = bufr.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } } private static class jecho { public static void main(String[] args) { System.out.println("Hello World"); } } }
jecho
是类CommandLineExec
的一个内部静态类,其作用就是输出字符串Hello World
。命令java Test$jecho > test.txt
在命令行中输入时,可以成功将类Test$jecho
输出的Hello World
重定向到文件test.txt
中,但是在上述代码中进行调用时,其运行结果如下:Hello World Process exitValue: 0
可以看到,命令显示运行成功,但是
test.txt
却并没有生成。这是由于Runtime.exec(..)
并不能解析重定向意图,Runtime.exec(..)
只能执行单一一个可执行程序(外部程序或脚本文件)。如果想将外部程序输出重定向或者通过管道发送给另一个进程,有两种方法:
- 手动进行编码,如下代码所示:
public class CommandLineExec { public static void main(String[] args) throws IOException, InterruptedException { FileOutputStream fos = new FileOutputStream("test.txt"); Process pid = Runtime.getRuntime().exec("java Test$jecho"); new Thread(new OutputHandlerRunnable(pid.getInputStream(), fos)).start(); new Thread(new OutputHandlerRunnable(pid.getErrorStream())).start(); int exitValue = pid.waitFor(); System.out.println("Process exitValue: " + exitValue); fos.flush(); fos.close(); } private static class OutputHandlerRunnable implements Runnable { private InputStream in; private OutputStream os; public OutputHandlerRunnable(InputStream in) { this.in = in; } // add redirect public OutputHandlerRunnable(InputStream in, OutputStream redirect) { this(in); this.os = redirect; } @Override public void run() { try (BufferedReader bufr = new BufferedReader(new InputStreamReader(this.in))) { PrintWriter pw = null; if (this.os != null) { pw = new PrintWriter(this.os); } String line = null; while ((line = bufr.readLine()) != null) { System.out.println(line); if (pw != null) { // redirect pw.println(line); } } if (pw != null) { pw.flush(); } } catch (IOException e) { e.printStackTrace(); } } } private static class jecho { public static void main(String[] args) { System.out.println("Hello World"); } } }
- 另一种更简便的办法就是:无法完成重定向功能是由于
Runtime.exec(..)
无法识别重定向符号,但是系统命令行工具可以识别,那么我们使用Runtime.exec(..)
启动命令行工具,让且执行命令并解析即可:
执行public class CommandLineExec { public static void main(String[] args) throws IOException, InterruptedException { Process pid = Runtime.getRuntime().exec("cmd /c java Test$jecho > test.txt"); new Thread(new OutputHandlerRunnable(pid.getInputStream())).start(); new Thread(new OutputHandlerRunnable(pid.getErrorStream())).start(); int exitValue = pid.waitFor(); System.out.println("Process exitValue: " + exitValue); }
javac CommandLineExec.java && java CommandLineExec
后就可以看到我们成功完成了重定向功能,test.txt
也成功生成了。
工具类
根据上文内容,将Runtime.exec(..)
稍微进行封装,写成一个工具类,方便调用,代码如下:
public final class ShellUtils {
private static ExecutorService threadpool = Executors.newCachedThreadPool();
private ShellUtils() {
}
private static boolean run(boolean waitFor, String[] command, OnCommandExecOutputListener listener) {
boolean ret = false;
try {
Process pid = Runtime.getRuntime().exec(command);
ShellUtils.threadpool.submit(new OutputHandler(pid.getInputStream(), OutputHandler.TYPE_RETRIVE_OUTPUTSTREAM, listener));
ShellUtils.threadpool.submit(new OutputHandler(pid.getErrorStream(), OutputHandler.TYPE_RETRIVE_ERRORSTREAM, listener));
ret = (waitFor ? pid.waitFor() : pid.exitValue()) == 0;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalThreadStateException e) {
e.printStackTrace();
}
return ret;
}
public static boolean runAsync(String[] command, OnCommandExecOutputListener listener) {
return ShellUtils.run(false, command, listener);
}
public static boolean run(String[] command, OnCommandExecOutputListener listener) {
return ShellUtils.run(true, command, listener);
}
public interface OnCommandExecOutputListener {
void onSuccess(String line);
void onError(String line);
}
private static class OutputHandler implements Runnable {
private static int TYPE_RETRIVE_OUTPUTSTREAM = 0;
private static int TYPE_RETRIVE_ERRORSTREAM = 1;
private InputStream in;
private OnCommandExecOutputListener listener;
private int type;
public OutputHandler(InputStream in, int type, OnCommandExecOutputListener listener) {
this.in = in;
this.type = type;
this.listener = listener;
}
@Override
public void run() {
try (BufferedReader bufr = new BufferedReader(new InputStreamReader(this.in))) {
String line = null;
while ((line = bufr.readLine()) != null) {
if (this.listener != null) {
if (this.type == OutputHandler.TYPE_RETRIVE_ERRORSTREAM) {
this.listener.onError(line);
} else {
this.listener.onSuccess(line);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
调用方法:
String[] commands = {"cmd", "/c", "ping www.baidu.com /t"};
// 同步调用
ShellUtils.run(commands,new OnCommandExecOutputListener(){...});
// 异步调用
ShellUtils.runAsync(commands,new OnCommandExecOutputListener(){...});