[Java]使用Apache Commons Execs调用脚本

山子野
• 阅读 5969

概述

写这篇的主要目的是为了整理和记录,归档以便以后查阅。

我之前在SF上提问了一个问题:如何正确使用PipedInputStream和PipedOutputStream

问题中提到的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下对象调试程序很有帮助,因此,我们可以列表表示整个需求。

序号 需求 是否必须
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方法返回的是退出码、标准输出、错误输出和脚本提供的结果描述

1.3 通过getXXX方法的形式可以将具体的实现交给具体实现类来完成

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());
    }

}
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
Unity接入今日头条广告(激励广告)
   写这篇博客主要是为了使用Eclipse导出Jar包和Res文件去给Unity调用。之前我写过一篇博客(https://www.cnblogs.com/weiqiangwaideshijie/p/7715861.html(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.
Wesley13 Wesley13
3年前
MySQL数据库InnoDB存储引擎Log漫游(1)
作者:宋利兵来源:MySQL代码研究(mysqlcode)0、导读本文介绍了InnoDB引擎如何利用UndoLog和RedoLog来保证事务的原子性、持久性原理,以及InnoDB引擎实现UndoLog和RedoLog的基本思路。00–UndoLogUndoLog是为了实现事务的原子性,
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Stella981 Stella981
3年前
Linux日志安全分析技巧
0x00前言我正在整理一个项目,收集和汇总了一些应急响应案例(不断更新中)。GitHub地址:https://github.com/Bypass007/EmergencyResponseNotes本文主要介绍Linux日志分析的技巧,更多详细信息请访问Github地址,欢迎Star。0x01日志简介Lin
Stella981 Stella981
3年前
Python调用Ant构建时根据构建状态来决定命令行退出状态
在使用python执行Ant构建时遇到的问题:使用os.system()调用Ant构建时,不论构建成功还是失败(BUILDSUCCESSFUL/BUILDFAILED),命令行的总是正常退出要解决问题:首先想到的是获取ant命令的返回值,根据返回值来决定命令行的退出状态(0或非0,0代表正常退出)查阅相关资料,得知python调用系
Stella981 Stella981
3年前
Hibernate纯sql查询结果和该sql在数据库直接查询结果不一致
问题:今天在做一个查询的时候发现一个问题,我先在数据库实现了我需要的sql,然后我在代码中代码:selectdistinctd.id,d.name,COALESCE(c.count_num,0),COALESCE(c.count_fix,0),COALESCE(c
Stella981 Stella981
3年前
DevOps世界中的软件开发
!(https://oscimg.oschina.net/oscnet/f40e68cbfe8148deb00f040b4e917a0a.jpg)在整个软件开发过程中,开发人员通常需要花费大量时间来修复错误和漏洞,以便一切按计划进行交付。但是,通过DevOps实践,可以更轻松地管理和保护这些问题。这是由于以下事实:使用DevOps实践的软
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这