问题:
在Java程序中,通过Runtime.getRuntime().exec()执行一个Linux脚本导致程序被挂住,而在终端上直接执行这个脚本则没有任何问题。
原因:
先来看Java代码:
public final static void process1(String[] cmdarray) { Process p = null; BufferedReader br = null; try { p = Runtime.getRuntime().exec(cmdarray); br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } p.waitFor(); } catch (Exception e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if (p != null) { p.destroy(); } } }
脚本内容很简单,主要内容是将一个指定的tar.gz文件解压到指定目录中。
程序被挂住后,查看进程列表,发现了几个可疑点:
neil@-bash:~/work/tgz$ps ux | grep dowjones neil 2079 0.0 0.0 2435492 264 s001 R+ 10:56上午 0:00.00 egrep dowjones neil 2077 0.0 0.0 2435080 652 ?? S 10:56上午 0:00.24 tar xvf dowjones.tar.gz neil 2073 0.0 0.0 2435488 792 ?? S 10:56上午 0:00.00 /bin/bash /Users/neil/bin/genova/genova_crm.sh /Users/neil/work/tgz/dowjones.tar.gz /Users/neil/work/dest/dowj
其中genova_crm.sh 就是要执行的脚本,tar xvf dowjones.tar.gz 就是执行解压的命令。
可以看到,程序卡在tar命令上,这个命令被挂住了,非常奇怪的事情。。。
再次查看JDK文档,发现Process的文档上说标准缓冲区大小有限,不正确操作输入输出流时可能导致程序挂住。
单独执行tar xvf dowjones.tar.gz命令时,发现有N多输出,而通过Java执行时,没有看到那些输出。
Java程序中只获取了标准输出流,没有获取错误输出流,那么有可能是错误输出缓冲区满而导致tar命令挂住。
解决方法:
修改Java程序,标准输出流与错误输出流均要处理,保证输出缓冲区不会被堵住。具体作法是用一个异步线程读取标准输出,读完即扔,让主线程读取错误输出流:
public final static void process1(String[] cmdarray) { try { final Process p = Runtime.getRuntime().exec(cmdarray); new Thread(new Runnable() { @Override publicvoid run() { BufferedReader br = new BufferedReader( new InputStreamReader(p.getInputStream())); try { while (br.readLine() != null) ; br.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); BufferedReader br = null; br = new BufferedReader(new InputStreamReader(p.getErrorStream())); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } p.waitFor(); br.close(); p.destroy(); } catch (Exception e) { e.printStackTrace(); } }
重新执行,发现程序可以正常执行了,tar命令的回显被打印出来了。问题解决。
这可能跟特定的tar包有关,执行tar解压时,明显可以看到回显字符串中有些乱码,回显全部被输出到错误流了。
上述方法可以避免标准输出或错误输出缓冲区满从而挂住主程序的问题,但是需要同时处理两个流,有重复之嫌。如果能把标准输出和错误输出并为一个流,那只需要处理一个流即可。ProcessBuilder提供了这种能力。
创建Process有两种方式,一种就是上述的通Runtime.exec来得到,还有一种可以通ProcessBuilder.start()来产生一个Process实例。
ProcessBuilder可以先设置必要的参数数据,如命令、环境变量、工作目录、重定向错误流到标准输出,然后start()根据这些参数来生成一个Process实例,启动一个子进程来执行相应的命令。
代码如下:
public final static void process(String[] cmdarray) throws Throwable { ProcessBuilder pb = new ProcessBuilder(cmdarray); pb.redirectErrorStream(true); Process p = null; BufferedReader br = null; try { p = pb.start(); br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line = null; logger.info("Invoke shell: {}", StringUtils.join(cmdarray, " ")); while ((line = br.readLine()) != null) { logger.info(line); } p.waitFor(); } finally { if (br != null) { br.close(); } if (p != null) { p.destroy(); } } }
通过上述代码可以看到,错误流被重定向到标准输出流,那么程序只需要处理标准输出就可以了。