[Java]使用Apache Commons Execs调用脚本 - Honwhy
概述
写这篇的主要目的是为了整理和记录,归档以便以后查阅。
我之前在SF上提问了一个问题:如何正确使用PipedInputStream和PipedOutputStream
问题中提到的Apache Commons Execs这个库,相比我们原来使用原生的Runtime和Process有不少优点。
对比我之前写过的代码,总结一下:
简化路径处理
如果要调用的脚本的路径存在空格,Apache Commons Execs会自动帮忙加上转义字符
兼容Windows环境
使用原生Runtime和Process方式时,必须手工为调用bat脚本加上`cmd /c`,比如把`test.bat`脚本拼接成`cmd /c`才向Runtime.exec方法传入这个脚本作为第一个参数
支持超时设置
原生的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); //设置处理标注输出和错误输出的Handlerint 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"); //设置编码
第二种,参考这个答案。
第二种是无法设置字符编码的,而第一种是获得了整个标准输出和错误输出后再设置字符编码的。
如果采用这种方式,为了满足从标准输出解析某个特殊结果是需要对这个标准输出做切分,再循环判断的。
最后我采用的是PipedInputStream
和PipedOutStream
的方式,这也是为什么会有这个问题如何正确使用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()); }}