java 执行shell命令及日志收集避坑指南

作者 | 唯一浩哥
来源 | urlify.cn/jyMfAn
 有时候我们需要调用系统命令执行一些东西 , 可能是为了方便 , 也可能是没有办法必须要调用 。 涉及执行系统命令的东西 , 则就不能做跨平台了 , 这和java语言的初衷是相背的 。
废话不多说 , java如何执行shell命令?自然是调用java语言类库提供的接口API了 。
1. java执行shell的api执行shell命令 , 可以说系统级的调用 , 编程语言自然必定会提供相应api操作了 。 在java中 , 有两个api供调用:Runtime.exec(), Process API. 简单使用如下:
1.1. Runtime.exec() 实现调用实现如下:
import java.io.InputStream; public class RuntimeExecTest {@Testpublic static void testRuntimeExec() {try {Process process = Runtime.getRuntime().exec("cmd.exe /c dir");process.waitFor();}catch (Exception e) {e.printStackTrace();}}}简单的说就是只有一行调用即可:Runtime.getRuntime().exec("cmd.exe /c dir") ; 看起来非常简洁 。
1.2. ProcessBuilder 实现使用ProcessBuilder需要自己操作更多东西 , 也因此可以自主设置更多东西 。 (但实际上底层与Runtime是一样的了) , 用例如下:
public class ProcessBuilderTest {@Testpublic void testProcessBuilder() {ProcessBuilder processBuilder = new ProcessBuilder();processBuilder.command("ipconfig");//将标准输入流和错误输入流合并 , 通过标准输入流读取信息processBuilder.redirectErrorStream(true);try {//启动进程Process start = processBuilder.start();//获取输入流InputStream inputStream = start.getInputStream();//转成字符输入流InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "gbk");int len = -1;char[] c = new char[1024];StringBuffer outputString = new StringBuffer();//读取进程输入流中的内容while ((len = inputStreamReader.read(c)) != -1) {String s = new String(c, 0, len);outputString.append(s);System.out.print(s);}inputStream.close();}catch (IOException e) {e.printStackTrace();}}}看起来是要麻烦些 , 但实际上是差不多的 , 只是上一个用例没有处理输出日志而已 。 但总体来说的 ProcessBuilder 的可控性更强 , 所以一般使用这个会更自由些 。
以下Runtime.exec()的实现:
// java.lang.Runtime#execpublic Process exec(String[] cmdarray, String[] envp, File dir)throws IOException {// 仅为 ProcessBuilder 的一个封装return new ProcessBuilder(cmdarray).environment(envp).directory(dir).start();}2. 调用shell思考事项从上面来看 , 要调用系统命令 , 并非难事 。 那是否就意味着我们可以随便调用现成方案进行处理工作呢?当然不是 , 我们应当要考虑几个问题?
1. 调用系统命令是进程级别的调用;进程与线程的差别大家懂的 , 更加重量级 , 开销更大 。 在java中 , 我们更多的是使用多线程进行并发 。 但如果用于系统调用 , 那就是进程级并发了 , 而且外部进程不再受jvm控制 , 出了问题也就不好玩了 。 所以 , 不要随便调用系统命令是个不错的实践 。2. 调用系统命令是硬件相关的调用;java语言的思想是一次编写 , 到处使用 。 但如果你使用的系统调用 , 则不好处理了 , 因为每个系统支持的命令并非完全一样的 , 你的代码也就会因环境的不一样而表现不一致了 。 健壮性就下来了 , 所以 , 少用为好 。3. 内存是否够用?一般我们jvm作为一个独立进程运行 , 会被分配足够多的内存 , 以保证运行的顺畅与高效 。 这时 , 可能留给系统的空间就不会太多了 , 而此时再调用系统进程运行业务 , 则得提前预估下咯 。4. 进程何时停止?当我调起一个系统进程之后 , 我们后续如何操作?比如是异步调用的话 , 可能就忽略掉结果了 。 而如果是同步调用的话 , 则当前线程必须等待进程退出 , 这样会让我们的业务大大简单化了 。 因为异步需要考虑的事情往往很多 。5. 如何获取进程日志信息?一个shell进程的调用 , 可能是一个比较耗时的操作 , 此时应该是只要任何进度 , 就应该汇报出来 , 从而避免外部看起来一直没有响应 , 从而无法判定是死掉了还是在运行中 。 而外部进程的通信 , 又不像一个普通io的调用 , 直接输出结果信息 。 这往往需要我们通过两个输出流进行捕获 。 而如何读取这两个输出流数据 , 就成了我们获取日志信息的关键了 。 ProcessBuilder 是使用inputStream 和 errStream 来表示两个输出流, 分别对应操作系统的标准输出流和错误输出流 。 但这两个流都是阻塞io流 , 如果处理不当 , 则会引起系统假死的风险 。6. 进程的异常如何捕获?在jvm线程里产生的异常 , 可以很方便的直接使用try...catch... 捕获 , 而shell调用的异常呢?它实际上并不能直接抛出异常 , 我们可以通过进程的返回码来判定是否发生了异常 , 这些错误码一般会遵循操作系统的错误定义规范 , 但时如果是我们自己写的shell或者其他同学写的shell就无法保证了 。 所以 , 往往除了我们要捕获错误之外 , 至少要规定0为正确的返回码 。 其他错误码也尽量不要乱用 。 其次 , 我们还应该在发生错误时 , 能从错误输出流信息中 , 获取到些许的蛛丝马迹 , 以便我们可以快速排错 。