java Runtime.exec()執(zhí)行shell/cmd命令:常見的幾種陷阱與一種完善實(shí)現(xiàn)

Runtime.getRuntime().exec()執(zhí)行JVM之外的程序:常見的幾種陷阱

前言

日常java開發(fā)中,有時(shí)需要通過java運(yùn)行其它應(yīng)用功程序娶耍,比如shell命令等摧莽。jdk的Runtime類提供了這樣的方法雄妥。首先來看Runtime類的文檔, 從文檔中可以看出培己,每個(gè)java程序只會有一個(gè)Runtime實(shí)例索抓,顯然這是一個(gè)單例模式钧忽。

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 */
 public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

    ......
}

要運(yùn)行JVM中外的程序毯炮,Runtime類提供了如下方法,詳細(xì)使用方法可參見源碼注釋

public Process exec(String command) throws IOException

public Process exec(String cmdarray[]) throws IOException

public Process exec(String command, String[] envp) throws IOException

public Process exec(String command, String[] envp, File dir) throws IOException

public Process exec(String[] cmdarray, String[] envp) throws IOException

public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException

通過這種方式運(yùn)行外部程序耸黑,有幾個(gè)陷阱需要注意桃煎,本文嘗試總結(jié)常見的幾個(gè)陷阱,并給出相應(yīng)的解決方法大刊。同時(shí)封裝一種比較完善的工具類为迈,用來運(yùn)行外部應(yīng)用,并提供超時(shí)功能缺菌。

Runtime.exec()常見的幾種陷阱以及避免方法

陷阱1:IllegalThreadStateException

通過exec執(zhí)行java命令為例子葫辐,最簡單的方式如下。執(zhí)行exec后男翰,通過Process獲取外部進(jìn)程的返回值并輸出另患。

import java.io.IOException;

/**
 * Created by yangjinfeng02 on 2016/4/27.
 */
public class Main {

    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        try {
            Process process = runtime.exec("java");
            int exitVal = process.exitValue();
            System.out.println("process exit value is " + exitVal);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

很遺憾的是纽乱,我們發(fā)現(xiàn)輸出結(jié)果如下蛾绎,拋出了IllegalThreadStateException異常

Exception in thread "main" java.lang.IllegalThreadStateException: process has not exited
at java.lang.ProcessImpl.exitValue(ProcessImpl.java:443)
at com.baidu.ubqa.agent.runner.Main.main(Main.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

為什么會拋出IllegalThreadStateException異常?

這是因?yàn)橥獠烤€程還沒有結(jié)束鸦列,這個(gè)時(shí)候去獲取退出碼租冠,exitValue()方法拋出了異常∈磬停看到這里讀者可能會問顽爹,為什么這個(gè)方法不能阻塞到外部進(jìn)程結(jié)束后再返回呢?確實(shí)如此骆姐,Process有一個(gè)waitFor()方法镜粤,就是這么做的,返回的也是退出碼玻褪。因此肉渴,我們可以用waitFor()方法替換exitValue()方法。

陷阱2:Runtime.exec()可能hang住带射,甚至死鎖

首先看下Process類的文檔說明

 * <p>By default, the created subprocess does not have its own terminal
 * or console.  All its standard I/O (i.e. stdin, stdout, stderr)
 * operations will be redirected to the parent process, where they can
 * be accessed via the streams obtained using the methods
 * {@link #getOutputStream()},
 * {@link #getInputStream()}, and
 * {@link #getErrorStream()}.
 * The parent process uses these streams to feed input to and get output
 * from the subprocess.  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, or even deadlock.

從這里可以看出同规,Runtime.exec()創(chuàng)建的子進(jìn)程公用父進(jìn)程的流,不同平臺上窟社,父進(jìn)程的stream buffer可能被打滿導(dǎo)致子進(jìn)程阻塞券勺,從而永遠(yuǎn)無法返回。
針對這種情況灿里,我們只需要將子進(jìn)程的stream重定向出來即可关炼。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * Created by yangjinfeng02 on 2016/4/27.
 */
public class Main {

    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        try {
            Process process = runtime.exec("java");
            BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line;
            System.out.println("OUTPUT");
            while ((line = stdoutReader.readLine()) != null) {
                System.out.println(line);
            }
            System.out.println("ERROR");
            while ((line = stderrReader.readLine()) != null) {
                System.out.println(line);
            }
            int exitVal = process.waitFor();
            System.out.println("process exit value is " + exitVal);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

陷阱3:不同平臺上,命令的兼容性

如果要在windows平臺上運(yùn)行dir命令匣吊,如果直接指定命令參數(shù)為dir盗扒,會提示命令找不到跪楞。而且不同版本windows系統(tǒng)上,運(yùn)行改命令的方式也不一樣侣灶。對這宗情況甸祭,需要根據(jù)系統(tǒng)版本進(jìn)行適當(dāng)區(qū)分。

String osName = System.getProperty("os.name" );
String[] cmd = new String[3];
if(osName.equals("Windows NT")) {
    cmd[0] = "cmd.exe" ;
    cmd[1] = "/C" ;
    cmd[2] = args[0];
} else if(osName.equals("Windows 95")) {
    cmd[0] = "command.com" ;
    cmd[1] = "/C" ;
    cmd[2] = args[0];
}  
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);

陷阱4:錯(cuò)把Runtime.exec()的command參數(shù)當(dāng)做命令行

本質(zhì)上來講褥影,Runtime.exec()的command參數(shù)只是一個(gè)可運(yùn)行的命令或者腳本池户,并不等效于Shell解器或者Cmd.exe,如果你想進(jìn)行輸入輸出重定向,pipeline等操作凡怎,則必須通過程序來實(shí)現(xiàn)校焦。不能直接在command參數(shù)中做。例如统倒,下面的例子

Process process = runtime.exec("java -version > a.txt");

這樣并不會產(chǎn)出a.txt文件寨典。要達(dá)到這種目的,需通過編程手段實(shí)現(xiàn)房匆,如下

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;

/**
 * Created by yangjinfeng02 on 2016/4/27.
 */
class StreamGobbler extends Thread {
    InputStream is;
    String type;
    OutputStream os;

    StreamGobbler(InputStream is, String type) {
        this(is, type, null);
    }

    StreamGobbler(InputStream is, String type, OutputStream redirect) {
        this.is = is;
        this.type = type;
        this.os = redirect;
    }

    public void run() {
        try {
            PrintWriter pw = null;
            if (os != null)
                pw = new PrintWriter(os);

            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String line;
            while ((line = br.readLine()) != null) {
                if (pw != null)
                    pw.println(line);
                System.out.println(type + ">" + line);
            }
            if (pw != null)
                pw.flush();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String args[]) {
        try {
            FileOutputStream fos = new FileOutputStream("logs/a.log");
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec("cmd.exe /C dir");

            // 重定向輸出流和錯(cuò)誤流
            StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");
            StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT", fos);

            errorGobbler.start();
            outputGobbler.start();
            int exitVal = proc.waitFor();
            System.out.println("ExitValue: " + exitVal);
            fos.flush();
            fos.close();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

一個(gè)比較完善的工具類

下面提供一種比較完善的實(shí)現(xiàn)耸成,提供了超時(shí)功能。

封裝返回結(jié)果

/**
* ExecuteResult.java
*/
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class ExecuteResult {
    private int exitCode;
    private String executeOut;

    public ExecuteResult(int exitCode, String executeOut) {
        this.exitCode = exitCode;
        this.executeOut = executeOut;
    }
}

對外接口

/**
* LocalCommandExecutor.java
*/
public interface LocalCommandExecutor {
    ExecuteResult executeCommand(String command, long timeout);
}

StreamGobbler類浴鸿,用來完成stream的管理

/**
* StreamGobbler.java
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StreamGobbler extends Thread {
    private static Logger logger = LoggerFactory.getLogger(StreamGobbler.class);
    private InputStream inputStream;
    private String streamType;
    private StringBuilder buf;
    private volatile boolean isStopped = false;

    /**
     * @param inputStream the InputStream to be consumed
     * @param streamType  the stream type (should be OUTPUT or ERROR)
     */
    public StreamGobbler(final InputStream inputStream, final String streamType) {
        this.inputStream = inputStream;
        this.streamType = streamType;
        this.buf = new StringBuilder();
        this.isStopped = false;
    }

    /**
     * Consumes the output from the input stream and displays the lines consumed
     * if configured to do so.
     */
    @Override
    public void run() {
        try {
            // 默認(rèn)編碼為UTF-8井氢,這里設(shè)置編碼為GBK,因?yàn)閃IN7的編碼為GBK
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "GBK");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                this.buf.append(line + "\n");
            }
        } catch (IOException ex) {
            logger.trace("Failed to successfully consume and display the input stream of type " + streamType + ".", ex);
        } finally {
            this.isStopped = true;
            synchronized (this) {
                notify();
            }
        }
    }

    public String getContent() {
        if (!this.isStopped) {
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException ignore) {
                    ignore.printStackTrace();
                }
            }
        }
        return this.buf.toString();
    }
}

實(shí)現(xiàn)類

通過SynchronousQueue隊(duì)列保證只有一個(gè)線程在獲取外部進(jìn)程的退出碼岳链,由線程池提供超時(shí)功能花竞。

/**
* LocalCommandExecutorImpl.java
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class LocalCommandExecutorImpl implements LocalCommandExecutor {

    static final Logger logger = LoggerFactory.getLogger(LocalCommandExecutorImpl.class);

    static ExecutorService pool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 3L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());

    public ExecuteResult executeCommand(String command, long timeout) {
        Process process = null;
        InputStream pIn = null;
        InputStream pErr = null;
        StreamGobbler outputGobbler = null;
        StreamGobbler errorGobbler = null;
        Future<Integer> executeFuture = null;
        try {
            logger.info(command.toString());
            process = Runtime.getRuntime().exec(command);
            final Process p = process;

            // close process's output stream.
            p.getOutputStream().close();

            pIn = process.getInputStream();
            outputGobbler = new StreamGobbler(pIn, "OUTPUT");
            outputGobbler.start();

            pErr = process.getErrorStream();
            errorGobbler = new StreamGobbler(pErr, "ERROR");
            errorGobbler.start();

            // create a Callable for the command's Process which can be called by an Executor
            Callable<Integer> call = new Callable<Integer>() {
                public Integer call() throws Exception {
                    p.waitFor();
                    return p.exitValue();
                }
            };

            // submit the command's call and get the result from a
            executeFuture = pool.submit(call);
            int exitCode = executeFuture.get(timeout, TimeUnit.MILLISECONDS);
            return new ExecuteResult(exitCode, outputGobbler.getContent());

        } catch (IOException ex) {
            String errorMessage = "The command [" + command + "] execute failed.";
            logger.error(errorMessage, ex);
            return new ExecuteResult(-1, null);
        } catch (TimeoutException ex) {
            String errorMessage = "The command [" + command + "] timed out.";
            logger.error(errorMessage, ex);
            return new ExecuteResult(-1, null);
        } catch (ExecutionException ex) {
            String errorMessage = "The command [" + command + "] did not complete due to an execution error.";
            logger.error(errorMessage, ex);
            return new ExecuteResult(-1, null);
        } catch (InterruptedException ex) {
            String errorMessage = "The command [" + command + "] did not complete due to an interrupted error.";
            logger.error(errorMessage, ex);
            return new ExecuteResult(-1, null);
        } finally {
            if (executeFuture != null) {
                try {
                    executeFuture.cancel(true);
                } catch (Exception ignore) {
                    ignore.printStackTrace();
                }
            }
            if (pIn != null) {
                this.closeQuietly(pIn);
                if (outputGobbler != null && !outputGobbler.isInterrupted()) {
                    outputGobbler.interrupt();
                }
            }
            if (pErr != null) {
                this.closeQuietly(pErr);
                if (errorGobbler != null && !errorGobbler.isInterrupted()) {
                    errorGobbler.interrupt();
                }
            }
            if (process != null) {
                process.destroy();
            }
        }
    }

    private void closeQuietly(Closeable c) {
        try {
            if (c != null) {
                c.close();
            }
        } catch (IOException e) {
            logger.error("exception", e);
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掸哑,隨后出現(xiàn)的幾起案子约急,更是在濱河造成了極大的恐慌,老刑警劉巖苗分,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厌蔽,死亡現(xiàn)場離奇詭異,居然都是意外死亡俭嘁,警方通過查閱死者的電腦和手機(jī)躺枕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來供填,“玉大人拐云,你說我怎么就攤上這事〗” “怎么了叉瘩?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粘捎。 經(jīng)常有香客問我薇缅,道長危彩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任泳桦,我火速辦了婚禮汤徽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灸撰。我一直安慰自己谒府,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布浮毯。 她就那樣靜靜地躺著完疫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪债蓝。 梳的紋絲不亂的頭發(fā)上壳鹤,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音饰迹,去河邊找鬼芳誓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蹦锋,可吹牛的內(nèi)容都是我干的兆沙。 我是一名探鬼主播欧芽,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼莉掂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了千扔?” 一聲冷哼從身側(cè)響起憎妙,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎曲楚,沒想到半個(gè)月后厘唾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡龙誊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年抚垃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趟大。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹤树,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逊朽,到底是詐尸還是另有隱情罕伯,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布叽讳,位于F島的核電站追他,受9級特大地震影響坟募,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邑狸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一懈糯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧单雾,春花似錦昂利、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至硬萍,卻和暖如春扩所,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朴乖。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工祖屏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人买羞。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓袁勺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親畜普。 傳聞我的和親對象是個(gè)殘疾皇子期丰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)吃挑,斷路器钝荡,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 1.import static是Java 5增加的功能,就是將Import類中的靜態(tài)方法,可以作為本類的靜態(tài)方法來...
    XLsn0w閱讀 1,212評論 0 2
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法舶衬,類相關(guān)的語法埠通,內(nèi)部類的語法,繼承相關(guān)的語法逛犹,異常的語法端辱,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,748評論 6 342
  • 1、 在感情的世界里虽画,曾經(jīng)有無數(shù)的哲學(xué)家舞蔽、心理學(xué)家、與作家孜孜不倦的去探索過狸捕,但是得出的結(jié)論喷鸽,卻太過于形而上學(xué)。以...
    陳大白閱讀 373評論 0 2