[Java]使用Apache Commons Execs調用腳本

概述

寫這篇的主要目的是爲了整理和記錄,歸檔以便之後查閱。segmentfault

我以前在SF上提問了一個問題:如何正確使用PipedInputStream和PipedOutputStreamapp

問題中提到的Apache Commons Execs這個庫,相比咱們原來使用原生的Runtime和Process有很多優勢。
對比我以前寫過的代碼,總結一下:異步

  1. 簡化路徑處理
    若是要調用的腳本的路徑存在空格,Apache Commons Execs會自動幫忙加上轉義字符
  2. 兼容Windows環境
    使用原生Runtime和Process方式時,必須手工爲調用bat腳本加上cmd /c,好比把test.bat腳本拼接成cmd /c才向Runtime.exec方法傳入這個腳本做爲第一個參數
  3. 支持超時設置
    原生的Runtime和Process並無直接支持超時的設置,但網上也有在原生基礎上作的超時功能的封裝,大概是基於循環按期檢查的機制。在SF上也有相似的文章,其中的代碼大可參考一下,我要提醒的是,須要注意異步線程不能給及時返回結果的問題。

在個人項目需求中,規定要得到腳本的退出碼,標準輸出、錯誤輸出。另外,還有可能要從標註輸出中解析獲得一個描述成功或失敗的結果,大概就是過濾腳本的標準輸出,捕獲感興趣的某一行,最後要預留超時設置的接口。還有,須要支持字符編碼設置,在Windows下對象調試程序頗有幫助,所以,咱們能夠列表表示整個需求。ide

序號 需求 是否必須
1 退出碼、標準輸出、錯誤輸出
2 得到腳本提供的結果描述
3 設置超時
4 設置字符編碼

設計思路

1. 定義抽象類預製總體流程

public abstract class AbstractCommonExecs {
    private String bin; //腳本
    private List<String> arguments; //參數
    
    //Constructor method
    
    //封裝返回結果
    public ExecResult exec() throws IOException {
        try{
            Executor executor = getExecutor();  //執行線程
            CommandLine cmdLine = getCommandLine(); //腳本命令參數等
            if(supportWatchdog()) { //是否支持監視 用於設置超時等
                executor.setWatchdog(getWatchdog());    
            }
            executor.setStreamHandler(streamHandler);   //設置處理標註輸出和錯誤輸出的Handler
            int ret = executor.execute(cmdLine);    //得到退出碼
        }catch(ExecuteException e) {
            int ret = e.getExitValue(); //若是出現異常還能得到退出碼 關於這個仔細想一想
        }
    }    
}

1.1 抽象類接收腳本和參數,類型和形式還能夠是別的形式測試

1.2 對外提供的exec方法返回的是退出碼、標準輸出、錯誤輸出和腳本提供的結果描述ui

1.3 經過getXXX方法的形式能夠將具體的實現交給具體實現類來完成this

2. 如何處理輸出

爲了從Executor中得到標準輸出和錯誤輸出,是須要向Executor傳入一個streamHandler的是,這是一個基於字節流式的Handler,爲了支持字符編碼的設計,
最終處理時咱們還須要將它轉成字符流並設置目標字符編碼,好比在Windows開發環境下設置爲GBK編碼

executor.setStreamHandler(streamHandler);   //設置處理標註輸出和錯誤輸出的Handler

這裏先提兩種很是有效的作法,一種是基於ByteArrayOutStream的,一種是官方封裝的LogOutputStream。第一種,線程

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();  
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();  

PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream,errorStream);  

executor.setStreamHandler(streamHandler);  

exec.execute(cmdline);  

String out = outputStream.toString("gbk");  //設置編碼

String error = errorStream.toString("gbk"); //設置編碼

第二種,參考這個答案設計

第二種是沒法設置字符編碼的,而第一種是得到了整個標準輸出和錯誤輸出後再設置字符編碼的。
若是採用這種方式,爲了知足從標準輸出解析某個特殊結果是須要對這個標準輸出作切分,再循環判斷的。

最後我採用的是PipedInputStreamPipedOutStream的方式,這也是爲何會有這個問題如何正確使用PipedInputStream和PipedOutputStream
。爲了讓處理標註輸出、錯誤輸出和結果描述看起來比較統一,我使用了回調的方式。

3. 回調方式處理

private void readInputStream(PipedInputStream pis, OutputCallback ...cbs) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(pis, getEncoding()));
    String line = null;
    while((line = br.readLine()) != null) {
        for(OutputCallback cb : cbs) {
            cb.parse(line); //這裏能夠得到結果描述
        }
    }
}

4. 說明

總體思路上的抽象已經作到了,可是還不夠完全,抽象類exec方法體內業務邏輯仍是過於耦合的。

完整代碼

ExecResult代碼,

public class ExecResult {

    private int exitCode;
    private String stdout;
    private String stderr;
    private String codeInfo;
    //getter and setter
}

OutputCallback接口代碼,

public interface OutputCallback {
    public void parse(String line);
}

AbstractCommonExecs代碼,

public abstract class AbstractCommonExecs {

    private Logger log = LoggerFactory.getLogger(AbstractCommonExecs.class);
    private static final String DEFAULT_ENCODING = "UTF-8";
    private String encoding = DEFAULT_ENCODING;
    
    private String bin;
    private List<String> arguments;
    public AbstractCommonExecs(String bin, List<String> arguments) {
        this.bin = bin;
        this.arguments = arguments;
    }
    
    public ExecResult exec() throws IOException{
        ExecResult er = new ExecResult();
        //ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PipedOutputStream outputStream = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(outputStream);
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        CodeInfoCallback codeInfoCb = new CodeInfoCallback();
        StdOutputCallback stdoutCb = new StdOutputCallback();
        ErrorOutputCallback stderrCb = new ErrorOutputCallback();
        String stdout = null;
        String stderr = null;
        try {
            Executor executor = getExecutor();
            CommandLine cmdLine = getCommandLine();
            log.info("Executing script {}",cmdLine.toString());
            if(supportWatchdog()) {
                executor.setWatchdog(getWatchdog());
            }
            PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream,errorStream);
            executor.setStreamHandler(streamHandler);
            int ret = executor.execute(cmdLine);
            
            readInputStream(pis, stdoutCb, codeInfoCb);
            pis.close();
            readErrorStream(errorStream, stderrCb);
            stdout = join(stdoutCb.getLines());
            stderr = stderrCb.getErrors();
            log.info("output from script {} is {}", this.bin, stdout);
            log.info("error output from script {} is {}", this.bin, stderr);
            log.info("exit code from script {} is {}", this.bin, ret);
            er.setStdout(stdout);
            er.setStderr(stderr);
            er.setCodeInfo(codeInfoCb.getCodeInfo());
            er.setExitCode(ret);
            return er;
        } catch (ExecuteException e) {
            if(pis != null) {
                readInputStream(pis, stdoutCb, codeInfoCb);
                pis.close();
            }
            if(errorStream != null) {
                readErrorStream(errorStream, stderrCb);
            }
            stdout = join(stdoutCb.getLines());
            stderr = stderrCb.getErrors();
            int ret = e.getExitValue();
            log.info("output from script {} is {}", this.bin, stdout);
            log.info("error output from script {} is {}", this.bin, stderr);
            log.info("exit code from script {} is {}", this.bin, ret);
            er.setStdout(stdout);
            er.setStderr(stderr);
            er.setCodeInfo(codeInfoCb.getCodeInfo());
            er.setExitCode(ret);
            return er;
        }
        
    }
    /**
     * 接口回調的方式解析腳本的錯誤輸出
     * @param baos
     * @param cbs
     * @throws IOException
     */
    private void readErrorStream(ByteArrayOutputStream baos, OutputCallback ...cbs) throws IOException {
        String err =  baos.toString(getEncoding());
        for(OutputCallback cb : cbs) {
            cb.parse(err);
        }
    }
    /**
     * 接口回調的方式解析腳本的標準輸出
     * @param pis
     * @param cbs
     * @throws IOException
     */
    private void readInputStream(PipedInputStream pis, OutputCallback ...cbs) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(pis, getEncoding()));
        String line = null;
        while((line = br.readLine()) != null) {
            for(OutputCallback cb : cbs) {
                cb.parse(line);
            }
        }
    }
    public Executor getExecutor() {
        Executor executor = new DefaultExecutor();
        executor.setWorkingDirectory(new File(this.bin).getParentFile());
        return executor;
    }
    public CommandLine getCommandLine() {
        String fullCommand = bin + join(arguments);        
        return CommandLine.parse(fullCommand);
    }
    protected String join(List<String> arguments) {
        if(arguments == null || arguments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for(String arg : arguments) {
            sb.append(" ").append(arg);
        }
        return sb.toString();
    }
    
    /**
     * @return the encoding
     */
    protected String getEncoding() {
        return encoding;
    }

    /**
     * @param encoding the encoding to set
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }
    
    /**
     * @return the bin
     */
    protected String getBin() {
        return bin;
    }

    /**
     * @param bin the bin to set
     */
    public void setBin(String bin) {
        this.bin = bin;
    }

    /**
     * @return the arguments
     */
    protected List<String> getArguments() {
        return arguments;
    }

    /**
     * @param arguments the arguments to set
     */
    public void setArguments(List<String> arguments) {
        this.arguments = arguments;
    }

    public abstract boolean supportWatchdog();
    public abstract ExecuteWatchdog getWatchdog();
}

測試

1. 支持字符編碼設置的測試

public class GbkCommonExecs extends AbstractCommonExecs{

    /**
     * @param bin
     * @param arguments
     */
    public GbkCommonExecs(String bin, List<String> arguments) {
        super(bin, arguments);
    }

    /* (non-Javadoc)
     * @see com.bingosoft.proxy.helper.AbstractCommonExecs#supportWatchdog()
     */
    @Override
    public boolean supportWatchdog() {
        // TODO implement AbstractCommonExecs.supportWatchdog
        return false;
    }

    /* (non-Javadoc)
     * @see com.bingosoft.proxy.helper.AbstractCommonExecs#getWatchdog()
     */
    @Override
    public ExecuteWatchdog getWatchdog() {
        // TODO implement AbstractCommonExecs.getWatchdog
        return null;
    }
    
    //提供這個編碼便可
    public String getEncoding() {
        return "GBK";
    }
    public static void main(String[] args) throws IOException {
        String bin = "ping";
        String arg1 = "127.0.0.1";
        List<String> arguments = new ArrayList<String>();
        arguments.add(arg1);
        AbstractCommonExecs executable = new GbkCommonExecs(bin, arguments);
        ExecResult er = executable.exec();
        System.out.println(er.getExitCode());
        System.out.println(er.getStdout());
        System.out.println(er.getStderr());
    }

}

2. 支持超時設置的測試

設置監視狗就能設置超時

public class TimeoutCommonExecs extends AbstractCommonExecs{

    private Logger log = LoggerFactory.getLogger(TimeoutCommonExecs.class);
    
    private long timeout = 10 * 1000; // 10 seconds
    public TimeoutCommonExecs(String bin, List<String> arguments) {
        super(bin, arguments);
    }
    public TimeoutCommonExecs(String bin, List<String> arguments, long timeout) {
        super(bin, arguments);
        this.timeout = timeout;
    }
    public boolean supportWatchdog() {
        return true; // 使用監視狗 監視腳本執行超時的狀況
    }
    public ExecuteWatchdog getWatchdog() {
        ExecuteWatchdog watchdog = new ExecuteWatchdog(this.timeout);
        return watchdog;
    }

    /**
     * @return the timeout
     */
    public long getTimeout() {
        return timeout;
    }

    /**
     * @param timeout the timeout to set
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }
    
}

爲了方便在Windows下測試

public class TimeoutGbkCommonExecs extends TimeoutCommonExecs{

    public TimeoutGbkCommonExecs(String bin, List<String> arguments, long timeout) {
        super(bin, arguments, timeout);
        
    }
    //字符編碼設置
    public String getEncoding() {
        return "GBK";
    }
    public static void main(String[] args) throws IOException {
        String bin = "ping";
        String arg1 = "-t";   //不斷ping
        String arg2 = "127.0.0.1";
        List<String> arguments = new ArrayList<String>();
        arguments.add(arg1);
        arguments.add(arg2);
        AbstractCommonExecs executable = new TimeoutGbkCommonExecs(bin, arguments, 5 * 1000);
        ExecResult er = executable.exec();
        System.out.println(er.getExitCode());
        System.out.println(er.getStdout());
        System.out.println(er.getStderr());
    }

}
相關文章
相關標籤/搜索