2012年1月30日星期一

浅析Java执行外部命令的几个要点(1)——简单的使用范例以及在Cygwin上的注意点 - loveapple——郝春利的个人博客 - 博客频道 - CSDN.NET

浅析Java执行外部命令的几个要点(1)——简单的使用范例以及在Cygwin上的注意点 - loveapple——郝春利的个人博客 - 博客频道 - CSDN.NET

转贴请注明出处:http://blog.csdn.net/froole
作者:郝春利

在Java语言中执行外部命令,到JDK1.4,一直都是使用java.lang.Runtime。从JDK1.5版本之后导入了java.lang.ProcessBuilder,并且是用起来同样非常方便。
java.lang.Runtime的例子在网上已经太多,这里不做重复,举一个java.lang.ProcessBuilder的范例,并简单说明几个要点。

1.Runtime VS ProcessBuilder
跟Runtime相比,ProcessBuilder有个特点,被执行的命令可以同ProcessBuilder一起被初始化。
例如事先定一一个启动文本编辑器的命令,可以如下:
new ProcessBuilder("notepad.exe", "test.txt");
Runtime是直接运行exeuct,所以,如果要需要执行的命令是不变的,使用ProcessBuilder对命令进行初始化,这要比使用Runtime更加方便,而且,从某种角度说,正确使用ProcessBuilder可以避免一些生成外部命令时容易发生的错误。
但是,需要注意的事,老版本和新版本的功能是没有变化的,具体用哪个,还需要具体情况具体分析。
以下是一个使用ProcessBuilder的例子:
  1. ProcessBuilder pb = new ProcessBuilder("notepad.exe", "test.txt");
  2. try {
  3. Process p = pb.start();
  4. int ret = p.waitFor();
  5. System.out.println("process exited with value : " + ret);
  6. } catch (IOException e) {
  7. // start()命令的执行处理失败
  8. e.printStackTrace();
  9. } catch (InterruptedException e) {
  10. // waitFor()处理失败
  11. e.printStackTrace();
  12. }



2.外部程序执行之后,提取执行结果
无论使用Runtime.exeuc还是ProcessBuilder.start执行外部命令,都会返回一个Process实现。
外部命令执行后,向OS输出的所有返回结果都可以通过Process来取得。其中,用来表示命令执行结束状态的出口值,可以通过调用exitValue()或者waitFor()方法来提取。
但是exitValue()跟waitFor()有一个本质区别,调用waitFor()之后,程序并不会马上返回出口状态,而是一直等到外部程序执行结束,调用exitValue(),如果子程序没有结束,就会抛出一个IllegalThreadStateException异常
所以,在大多数情况下,通过调用waitFor()来取得外部程序的执行结果更加安全。
waitFor()的确是好,但是有时候却事与愿违。
例如,外部程序的执行时间超出了预定的执行时间(Timeout);外部程序可以正常执行,但是,在Java执行就无缘无故停在半路不动了。
在实际的外部程序运行中将会出现很多状况,《浅析Java执行外部命令的几个要点》将在今后的部分中作具体的介绍。

3.在Cygwin上使用Sun JVM最需要注意的地方
Cygwin是一款跑在Windows上的Unix虚拟软件。
Cygwin上默认Unix格式的文件路径,例如:/usr/bin/find,当然,C:/WINDOWS这样的路径也可以使用。
虽然Cygwin是虚拟的Unix环境,但是,里面的命令归根结底还是Win32内核下的执行程序,所以Cygwin上可以执行Windows下的命令。
这就给在Cygwin下执行Java程序的混乱带来了隐患,因为,Cygwin下,默认Unix路径,而在Cygwin下执行的Java是在Windows上的JVM。
现象及解决方法:
  • 要 在Cygwin下,把/home/xx这个路径传给Java程序,Java得到这个路径之后,将会默认为[系统盘:/home/xx],可是,实际这个 [/home/xx]是在[$Cygwin/home/xx],这个时候,就出现了混乱,Java无法找到所指定的路径在什么地方。这种情况的解决方法, 就是把/home/xx这个路径,用Windows上的绝对路径替代,如:[C:/Cygwinpath/home/xx]
Java下无法认出Windows文件系统的路径分隔符。当然,这个问题在DOS下同样存在。
例 如在Cygwin下执行/usr/bin/find命令,并将输出结果通过xargs传给Java程序,这个时候Java程序会报错,找不到所指定的路 径。原因是被带入到Java中的路径格式如[C:/WINDOWS/xxx]中,“/”在Java中的字串变量中,是默认的escape字符,也就是说, 传给Java的是[C:/WINDOWS/xxx],到了Java里面[C:WINDOWSxxx]。
很多系统都是在Windows下开发,Unix下运用,特别是后台程序,如果遇到处理文件系统路径的问题,需特别注意这一点。如果程序最终在Unix下运行,测试的时候,不妨把路径中的“/”改成“/”,如果实在不行,就只能拿到Unix下去测试。


用Java执行外部命令非常简单,只要在带入参数的时候注意不要把参数弄错就可以了。
但是,在实际运用中,还有一个比较棘手的问题,就是外部命令执行的timeout。
特别是执行时间比较长的外部命令,如外部的后台处理程序。
当执行这些程序的时候不可能任由他们随便跑,大多数时候,都要事先设定一个外部命令的最大执行时限,也就是timeout,如果超过这个时间程序还没有执行完成,那么将强制杀死程序,并输出错误日志。
JDK的外部执行API没有提供设定timeout的接口,所以,实现此功能,只能自行解决。

在判断执行结果的时候,将会一直对Process的返回值进行判断,前面的部分已经介绍了Process的使用特点,这里将不再重复,只放出代码,如下:
  1. import java.io.File;
  2. import java.io.IOException;
  3. /**
  4. * 支持timeout的执行外部命令的类定义。
  5. *
  6. * @version 1.0
  7. * @author hao_shunri
  8. * @see http://blog.csdn.net/froole
  9. */
  10. public class CommandExec {
  11. /**
  12. * 用来执行外部命令的{@link Runtime}实现
  13. */
  14. private Runtime runtime;
  15. /**
  16. * 默认确认timeout的间隔时间,単位:毫秒
  17. */
  18. private static final int DEFAULT_TIMEOUT_INTERVAL = 500;
  19. /**
  20. * 默认timeout
  21. */
  22. private static final long DEFAULT_TIMEOUT = 60 * 1000;
  23. /**
  24. * Timout时间,単位:毫秒
  25. */
  26. private long timeout = -1;
  27. /**
  28. * 确认timeout的间隔时间,単位:毫秒
  29. */
  30. private long interval;
  31. /**
  32. *
  33. * @param runtime 用来执行外部命令的{@link Runtime}实现
  34. * @param timeout timeout时间
  35. * @param invterval 换算timeout的间隔时间
  36. */
  37. public CommandExec(Runtime runtime, long timeout, long invterval){
  38. this.timeout = timeout;
  39. this.interval = invterval;
  40. this.runtime = runtime;
  41. }
  42. /**
  43. *
  44. *
  45. * @param timeout timeout时间
  46. * @param invterval 换算timeout的间隔时间
  47. */
  48. public CommandExec(long timeout, long invterval){
  49. this(Runtime.getRuntime(), timeout, invterval);
  50. }
  51. /**
  52. *
  53. *
  54. * @param timeout timeout时间
  55. */
  56. public CommandExec(long timeout){
  57. this(timeout, DEFAULT_TIMEOUT_INTERVAL);
  58. }
  59. /**
  60. *
  61. *
  62. */
  63. public CommandExec(){
  64. this(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_INTERVAL);
  65. }
  66. /**
  67. *
  68. * 执行外部命令
  69. *
  70. * @param commands
  71. * 命令数组
  72. * @return
  73. * @throws IOException
  74. */
  75. public Process exec(String[] commands) throws IOException, InterruptedException {
  76. return exec(commands, null);
  77. }
  78. /**
  79. * 执行外部命令
  80. *
  81. * @param commands
  82. * 命令数组
  83. * @param dir
  84. * 临时目录
  85. * @return
  86. * @throws IOException
  87. */
  88. public Process exec(String[] commands, File dir) throws IOException, InterruptedException {
  89. return exec(commands, null, dir);
  90. }
  91. /**
  92. *
  93. * 执行外部命令
  94. *
  95. * @param commands
  96. * 命令数组
  97. * @param envp
  98. * 环境变量
  99. * @param dir
  100. * 临时目录
  101. * @return
  102. * @throws IOException
  103. * @throws IllegalThreadStateException
  104. */
  105. public Process exec(String[] commands, String[] envp, File dir) throws IOException, InterruptedException {
  106. if (commands == null) {
  107. throw new NullPointerException();
  108. }
  109. Process process = runtime.exec(commands, envp, dir);
  110. // 设定timeout
  111. long limitTime = timeout + System.currentTimeMillis();
  112. // 状态
  113. Integer status = null;
  114. do {
  115. try {
  116. status = process.exitValue();
  117. break;
  118. } catch (IllegalThreadStateException e) {
  119. try {
  120. Thread.sleep(getInterval());
  121. } catch (InterruptedException we) {
  122. return null;
  123. }
  124. }
  125. } while (System.currentTimeMillis() < limitTime);
  126. if (status == null) {
  127. process.destroy();
  128. try {
  129. status = process.waitFor();
  130. } catch (InterruptedException e) {
  131. throw e;
  132. }
  133. }
  134. return process;
  135. }
  136. /**
  137. * 设定Timout时间,単位:毫秒
  138. *
  139. * @param timeout
  140. * Timout时间,単位:毫秒
  141. */
  142. public void setTimeout(long timeout) {
  143. this.timeout = timeout;
  144. }
  145. /**
  146. * 提取Timout时间,単位:毫秒
  147. *
  148. * @return Timout时间,単位:毫秒
  149. */
  150. public long getTimeout() {
  151. return timeout;
  152. }
  153. /**
  154. * 提取确认timeout的间隔时间,単位:毫秒
  155. *
  156. * @return 确认timeout的间隔时间,単位:毫秒
  157. */
  158. public long getInterval() {
  159. return interval;
  160. }
  161. /**
  162. * 设定确认timeout的间隔时间,単位:毫秒
  163. *
  164. * @param interval
  165. * 确认timeout的间隔时间,単位:毫秒
  166. */
  167. public void setInterval(long interval) {
  168. this.interval = interval;
  169. }
  170. }


上一部分定义了一个支持超时控制功能的类——CommandExec
在这部分,将展示CommandExec的使用方法,并且向读者展示如何取得命令的执行结果并将他们打印出来。

环境描述:
假设执行环境为Windows,在C盘(系统盘)上有一个test.zip文件,里面压缩一个test.txt文本文件。
用系统自带的unzip -l命令可以显示出压缩文件的内容。
并且,系统还有sleep命令,用来暂停处理。

执行处理步骤:

  1. 生成一个新的CommandExec实现,timeout为6秒
  2. 执行系统命令[unzip -l C:/test.zip],确认其正常结束并打印执行结果;
  3. 执行[sleep 7],让处理暂停7秒,但是,到了6秒的时候程序会被强制终止,从出口值可以判断[sleep 7]的执行结果是失败的;
  4. 执行[unzip -l C:/test.zip],由于Java中将“C:/test.zip”认为“C:test.zip”,由于找不到文件执行将失败,并打印出错误信息。

以下是代码,有兴趣的读者可以在自己的电脑上测试一下。

  1. import java.io.IOException;
  2. import java.io.InputStream;

  3. public class CommandExecTest {

  4. /**
  5. *
  6. * 执行结果:
  7. *
  8. *
  9. * processSuccess Result:0
  10. * processTimeout Result:1
  11. * processFail Result:9
  12. * >>>>>>>Sucess process:
  13. * ==================
  14. * sucess std:
  15. * Archive: C:/test.zip
  16. * Length Date Time Name
  17. * -------- ---- ---- ----
  18. * 0 08/12/25 20:09 test/test.txt
  19. * -------- -------
  20. * 0 1 file
  21. * ==================
  22. * error std:
  23. * ==================
  24. * >>>>>>>Timeout process:
  25. * ==================
  26. * sucess std:
  27. * ==================
  28. * error std:
  29. * ==================
  30. * >>>>>>>Fail process:
  31. * ==================
  32. * sucess std:
  33. * ==================
  34. * error std:
  35. * unzip: cannot find either C: est.zip or C: est.zip.zip.
  36. * ==================
  37. *
  38. *
  39. * @param args
  40. * @throws Exception
  41. */
  42. public static void main(String[] args) throws Exception {
  43. Runtime runtime = Runtime.getRuntime();
  44. long timeout = 6 * 1000L;
  45. long invterval = 500L;
  46. CommandExec exec = new CommandExec(runtime, timeout, invterval);

  47. // 执行成功
  48. String[] command = new String[] { "unzip", "-l", "C:/test.zip" };
  49. Process processSuccess = exec.exec(command);
  50. System.out.println("processSuccess Result:" + processSuccess.exitValue());

  51. // 超时执行
  52. command = new String[] { "sleep", "7" };
  53. Process processTimeout = exec.exec(command);
  54. System.out.println("processTimeout Result:" + processTimeout.waitFor());

  55. // 执行失败
  56. command = new String[] { "unzip", "-l", "C:/test.zip" };
  57. Process processFail = exec.exec(command);
  58. System.out.println("processFail Result:" + processFail.waitFor());

  59. // 打印结果
  60. System.out.println(">>>>>>>Sucess process:");
  61. dispProcess(processSuccess);
  62. System.out.println(">>>>>>>Timeout process:");
  63. dispProcess(processTimeout);
  64. System.out.println(">>>>>>>Fail process:");
  65. dispProcess(processFail);
  66. }

  67. /**
  68. *
  69. * 打印输出结果
  70. *
  71. * @param target
  72. * @throws IOException
  73. */
  74. static void dispProcess(Process target) throws IOException {
  75. InputStream stdIn = target.getInputStream();
  76. InputStream stdErr = target.getErrorStream();
  77. //
  78. try {
  79. System.out.println("==================");
  80. System.out.println("sucess std:");
  81. int c1;
  82. while ((c1 = stdIn.read()) != -1) {
  83. System.out.print((char) c1);
  84. }
  85. System.out.println("==================");
  86. System.out.println("error std:");
  87. int c2;
  88. while ((c2 = stdErr.read()) != -1) {
  89. System.out.print((char) c2);
  90. }
  91. System.out.println("==================");
  92. } finally {
  93. try {
  94. if (stdIn != null) {
  95. stdIn.close();
  96. }
  97. if (stdErr != null) {
  98. stdErr.close();
  99. }
  100. } catch (Exception e) {
  101. }
  102. }
  103. }
  104. }


在上一章已经验证了CommandExec可以很好的支持超时功能,通过它可以更方便的执行外部命令。
但是,这里还有一点需要注意——那就是shell(DOS)中的特殊符号。
因为用Java作为后台程序的系统,多运行于Unix/Linux,以下的介绍将基于如何shell来展开讨论。

假设,系统需要通过提取某个命令的标准输出,来进行某项处理。例如,搜索/路径下的xml文件并对结果处理,如下:

find / -name "*.xml" -type f | xargs java 相应程序
如果按照常规的方式,将以上的几个字串直接传递给Runtime.exec的参数,通过CommandExec的实现方法如下:
  1. CommandExec exec = new CommandExec();
  2. Process process = exec.exec(new String(){"find", "/", "-name", "/"*.xml/"", "-type", "f", "|", "xargs", "java", "相应程序"});

但是,实际结果将事与愿违,因为,在解析"|"的时候是以特殊符号来解析,而JVM将把"|"作为普通的字符串来处理。
在通常的处理中,这种功能可以屏蔽命令溢出的漏洞,但是在特殊需求的时候,就不方便了。
当然,在特殊需求下也有解决方法,可以如下执行以下命令:
  1. CommandExec exec = new CommandExec();
  2. Process process = exec.exec(new String(){"shell", "-c", "/"find / -name '*.xml' -type f | xargs java 相应程序/""});

也就是把要执行的命令作为shell的一个参数让Runtime.exec执行,为shell命令加上-c参数就可以正常执行目标命令了。

细心的读者已经看出,用上面的方法,有一个缺点就是,如果动态生成命令,很容易发生命令溢出的漏洞。
这里有一个解决方法,就是设定一个前提,被执行命令,文字串都必须以单引号包含,那样就比较容易escape了。
escape的方法可以如下定义,并添加到CommandExec当中。
  1. /**
  2. *
  3. * 对shell字串进行escape
  4. *
  5. * @param s 对象字串
  6. * @return 返回escape之后的shell字串
  7. */
  8. public static String escapeShellSpecialCharacters(String s) {

  9. StringBuilder sb = new StringBuilder(s.length() + 128);

  10. sb.append('/'');
  11. for (int i = 0; s != null && i < s.length(); i++) {
  12. char c = s.charAt(i);
  13. if (c == '/'') {
  14. sb.append('//');
  15. }
  16. sb.append(c);
  17. }
  18. sb.append('/'');
  19. return sb.toString();
  20. }
JVM执行外部命令功能的漏洞
到此为止,在Java中执行外部命令的方法以及注意点基本叙述完毕。但是,最后还有一点必须注意的就是JVM本身存在的一个漏洞。

这个漏洞的现象是,当被执行的外部命令,标准输出的量比较大的时候,程序会无故锁死。
特别是在JDK1.3版本尤为明显,在新版本中仍旧存在此漏洞。
不过,通过外部命令的执行方式,可以避免此漏洞的出现——通过重定向避免标准输出。
CommandExec的执行方法如下:
  1. CommandExec exec = new CommandExec();
  2. Process process = exec.exec(new String(){"shell", "-c", "/"find / -name '*.xml' -type f | xargs java 相应程序 > /dev/null/""});

到此结束,欢迎拍砖!



没有评论: