简析 Runtime.exec(..)

前言

Java 程序中,如果我们想执行一些 Shell 命令或其他外部应用程序,通常都是使用java.lang.Runtime.exec(..)方法来执行的。

当 Java 内置的 Runtime.exec(..) 方法在执行外部命令时,可能存在一些不易察觉的坑,往往会导致程序运行失败。

在网上看到一篇文章:『When Runtime.exec() won't』,里面对Runtime.exec(..)调用可能出现的坑进行了讲解,我觉得讲的很好,就记录一下。

简介

在简析Runtime.exec(..)可能出现的坑之前,先了解下该 API 的一些信息。

首先查看一下类Runtime的文档,如下图所示:

Runtime

每一个 Java 应用程序都拥有唯一的Runtime实例,使得该应用程序可以与其运行中的环境进行交互。该单例Runtime实例可以通过Runtime.getRuntime()进行获取。

Java 应用程序不能手动创建属于自己的一个Runtime实例。

简而言之,每个 Java 程序都拥有唯一一个Runtime实例,而Runtime实例的功能就是可以与运行时环境进行交互。
:所谓的运行时环境,个人理解,指的就是当前运行该 Java 程序(或更确切的说,当前运行的 JVM 进程)所在的环境,通常指的就是运行该程序的操作系统。

下面来看下Runtime.exe(..)方法,如下图所示:

Runtime.exec

从官方文档可以看到,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),所以看下该方法文档,如下图所示:

exec

Runtimie.exec(..)会在给定的环境和工作目录下启动一个独立的进程运行外部命令。其中,其参数含义如下:

  • cmdarray:字符串数组,包含要运行的命令和其参数。
  • envp:字符串数组,每个元素以name=value格式为子进程设置环境变量,为null则表示子进程继承当前进程的运行时环境。
  • dir:子进程的工作目录,为null则表示继承当前进程的工作目录。

举个栗子:

  1. 执行命令:dir D:,代码如下:
Runtime.getRuntime().exec("cmd /c dir D:");
  1. 为新启动的子进程设置环境变量(当外部应用内部需要某个环境变量设置后才能正常运行时),比如,设置环境变量name=vavlue
Runtime.getRuntime().exec("someApplication", new String[]{"name=value"});
  1. 执行 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(..)方法可能出现的陷阱。

陷阱

  1. 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()(推荐)等待外部程序正常运行结束。

  2. 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 程序的输出流。

  1. Shell 命令并不一定是可执行文件:当我们使用Runtime.exe(..)执行一些 Shell 命令(如dir/copy)时,可能会出现找不到可执行文件错误。这是因为这些 Shell 命令是属于命令行解释器的一部分,并不是可执行文件。因此要运行这些命令,需要带上命令行解释器,如下所示:
// for windows
Runtime.getRuntime().exec("cmd /c dir");
// for linux
Runtime.getRuntime().exec("/bin/bash ls");
  1. 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(){...});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 小编费力收集:给你想要的面试集合 1.C++或Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JA...
    八爷君阅读 4,565评论 1 114
  • //gradle 下载慢 //可以直接下载gradle之后放在对应的目录里//或者修改 根目录下的文件bul...
    zeromemcpy阅读 880评论 0 0
  • 接上篇,“傻白甜”继续向“做有故事的人”提问。 4、【I】环节,拆书家们常犯导致不过的错误有哪些? 回答:根据我遇...
    爱拆书的东哥阅读 1,018评论 2 4
  • 可以说日漫做的不错,但是近几年国漫也做得超级精致了,不同于日漫的萌,国漫也别具一格,今天就来说一下我心中的超级好看...
    梦呓Mengyi阅读 221评论 1 1
  • 今天早晨儿子睁开眼的第一句便是:“今天晚上要去大杨老师家吃饭。”这是多么期待呀!早晨上学告诉儿子在托管要快点写作业...
    刘璐_83ba阅读 72评论 0 1