转贴请注明出处: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的例子:
2.外部程序执行之后,提取执行结果
- ProcessBuilder pb = new ProcessBuilder("notepad.exe", "test.txt");
- try {
- Process p = pb.start();
- int ret = p.waitFor();
- System.out.println("process exited with value : " + ret);
- } catch (IOException e) {
- // start()命令的执行处理失败
- e.printStackTrace();
- } catch (InterruptedException e) {
- // waitFor()处理失败
- e.printStackTrace();
- }
无论使用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]
例 如在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的使用特点,这里将不再重复,只放出代码,如下:
- import java.io.File;
- import java.io.IOException;
- /**
- * 支持timeout的执行外部命令的类定义。
- *
- * @version 1.0
- * @author hao_shunri
- * @see http://blog.csdn.net/froole
- */
- public class CommandExec {
- /**
- * 用来执行外部命令的{@link Runtime}实现
- */
- private Runtime runtime;
- /**
- * 默认确认timeout的间隔时间,単位:毫秒
- */
- private static final int DEFAULT_TIMEOUT_INTERVAL = 500;
- /**
- * 默认timeout
- */
- private static final long DEFAULT_TIMEOUT = 60 * 1000;
- /**
- * Timout时间,単位:毫秒
- */
- private long timeout = -1;
- /**
- * 确认timeout的间隔时间,単位:毫秒
- */
- private long interval;
- /**
- *
- * @param runtime 用来执行外部命令的{@link Runtime}实现
- * @param timeout timeout时间
- * @param invterval 换算timeout的间隔时间
- */
- public CommandExec(Runtime runtime, long timeout, long invterval){
- this.timeout = timeout;
- this.interval = invterval;
- this.runtime = runtime;
- }
- /**
- *
- *
- * @param timeout timeout时间
- * @param invterval 换算timeout的间隔时间
- */
- public CommandExec(long timeout, long invterval){
- this(Runtime.getRuntime(), timeout, invterval);
- }
- /**
- *
- *
- * @param timeout timeout时间
- */
- public CommandExec(long timeout){
- this(timeout, DEFAULT_TIMEOUT_INTERVAL);
- }
- /**
- *
- *
- */
- public CommandExec(){
- this(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_INTERVAL);
- }
- /**
- *
- * 执行外部命令
- *
- * @param commands
- * 命令数组
- * @return
- * @throws IOException
- */
- public Process exec(String[] commands) throws IOException, InterruptedException {
- return exec(commands, null);
- }
- /**
- * 执行外部命令
- *
- * @param commands
- * 命令数组
- * @param dir
- * 临时目录
- * @return
- * @throws IOException
- */
- public Process exec(String[] commands, File dir) throws IOException, InterruptedException {
- return exec(commands, null, dir);
- }
- /**
- *
- * 执行外部命令
- *
- * @param commands
- * 命令数组
- * @param envp
- * 环境变量
- * @param dir
- * 临时目录
- * @return
- * @throws IOException
- * @throws IllegalThreadStateException
- */
- public Process exec(String[] commands, String[] envp, File dir) throws IOException, InterruptedException {
- if (commands == null) {
- throw new NullPointerException();
- }
- Process process = runtime.exec(commands, envp, dir);
- // 设定timeout
- long limitTime = timeout + System.currentTimeMillis();
- // 状态
- Integer status = null;
- do {
- try {
- status = process.exitValue();
- break;
- } catch (IllegalThreadStateException e) {
- try {
- Thread.sleep(getInterval());
- } catch (InterruptedException we) {
- return null;
- }
- }
- } while (System.currentTimeMillis() < limitTime);
- if (status == null) {
- process.destroy();
- try {
- status = process.waitFor();
- } catch (InterruptedException e) {
- throw e;
- }
- }
- return process;
- }
- /**
- * 设定Timout时间,単位:毫秒
- *
- * @param timeout
- * Timout时间,単位:毫秒
- */
- public void setTimeout(long timeout) {
- this.timeout = timeout;
- }
- /**
- * 提取Timout时间,単位:毫秒
- *
- * @return Timout时间,単位:毫秒
- */
- public long getTimeout() {
- return timeout;
- }
- /**
- * 提取确认timeout的间隔时间,単位:毫秒
- *
- * @return 确认timeout的间隔时间,単位:毫秒
- */
- public long getInterval() {
- return interval;
- }
- /**
- * 设定确认timeout的间隔时间,単位:毫秒
- *
- * @param interval
- * 确认timeout的间隔时间,単位:毫秒
- */
- public void setInterval(long interval) {
- this.interval = interval;
- }
- }
上一部分定义了一个支持超时控制功能的类——CommandExec
在这部分,将展示CommandExec的使用方法,并且向读者展示如何取得命令的执行结果并将他们打印出来。
环境描述:
假设执行环境为Windows,在C盘(系统盘)上有一个test.zip文件,里面压缩一个test.txt文本文件。
用系统自带的unzip -l命令可以显示出压缩文件的内容。
并且,系统还有sleep命令,用来暂停处理。
执行处理步骤:
- 生成一个新的CommandExec实现,timeout为6秒
- 执行系统命令[unzip -l C:/test.zip],确认其正常结束并打印执行结果;
- 执行[sleep 7],让处理暂停7秒,但是,到了6秒的时候程序会被强制终止,从出口值可以判断[sleep 7]的执行结果是失败的;
- 执行[unzip -l C:/test.zip],由于Java中将“C:/test.zip”认为“C:test.zip”,由于找不到文件执行将失败,并打印出错误信息。
以下是代码,有兴趣的读者可以在自己的电脑上测试一下。
- import java.io.IOException;
- import java.io.InputStream;
- public class CommandExecTest {
- /**
- *
- * 执行结果:
- *
- *
- * processSuccess Result:0
- * processTimeout Result:1
- * processFail Result:9
- * >>>>>>>Sucess process:
- * ==================
- * sucess std:
- * Archive: C:/test.zip
- * Length Date Time Name
- * -------- ---- ---- ----
- * 0 08/12/25 20:09 test/test.txt
- * -------- -------
- * 0 1 file
- * ==================
- * error std:
- * ==================
- * >>>>>>>Timeout process:
- * ==================
- * sucess std:
- * ==================
- * error std:
- * ==================
- * >>>>>>>Fail process:
- * ==================
- * sucess std:
- * ==================
- * error std:
- * unzip: cannot find either C: est.zip or C: est.zip.zip.
- * ==================
- *
- *
- * @param args
- * @throws Exception
- */
- public static void main(String[] args) throws Exception {
- Runtime runtime = Runtime.getRuntime();
- long timeout = 6 * 1000L;
- long invterval = 500L;
- CommandExec exec = new CommandExec(runtime, timeout, invterval);
- // 执行成功
- String[] command = new String[] { "unzip", "-l", "C:/test.zip" };
- Process processSuccess = exec.exec(command);
- System.out.println("processSuccess Result:" + processSuccess.exitValue());
- // 超时执行
- command = new String[] { "sleep", "7" };
- Process processTimeout = exec.exec(command);
- System.out.println("processTimeout Result:" + processTimeout.waitFor());
- // 执行失败
- command = new String[] { "unzip", "-l", "C:/test.zip" };
- Process processFail = exec.exec(command);
- System.out.println("processFail Result:" + processFail.waitFor());
- // 打印结果
- System.out.println(">>>>>>>Sucess process:");
- dispProcess(processSuccess);
- System.out.println(">>>>>>>Timeout process:");
- dispProcess(processTimeout);
- System.out.println(">>>>>>>Fail process:");
- dispProcess(processFail);
- }
- /**
- *
- * 打印输出结果
- *
- * @param target
- * @throws IOException
- */
- static void dispProcess(Process target) throws IOException {
- InputStream stdIn = target.getInputStream();
- InputStream stdErr = target.getErrorStream();
- //
- try {
- System.out.println("==================");
- System.out.println("sucess std:");
- int c1;
- while ((c1 = stdIn.read()) != -1) {
- System.out.print((char) c1);
- }
- System.out.println("==================");
- System.out.println("error std:");
- int c2;
- while ((c2 = stdErr.read()) != -1) {
- System.out.print((char) c2);
- }
- System.out.println("==================");
- } finally {
- try {
- if (stdIn != null) {
- stdIn.close();
- }
- if (stdErr != null) {
- stdErr.close();
- }
- } catch (Exception e) {
- }
- }
- }
- }
在上一章已经验证了CommandExec可以很好的支持超时功能,通过它可以更方便的执行外部命令。
但是,这里还有一点需要注意——那就是shell(DOS)中的特殊符号。
因为用Java作为后台程序的系统,多运行于Unix/Linux,以下的介绍将基于如何shell来展开讨论。
假设,系统需要通过提取某个命令的标准输出,来进行某项处理。例如,搜索/路径下的xml文件并对结果处理,如下:
find / -name "*.xml" -type f | xargs java 相应程序
如果按照常规的方式,将以上的几个字串直接传递给Runtime.exec的参数,通过CommandExec的实现方法如下:
- CommandExec exec = new CommandExec();
- Process process = exec.exec(new String(){"find", "/", "-name", "/"*.xml/"", "-type", "f", "|", "xargs", "java", "相应程序"});
但是,实际结果将事与愿违,因为,在解析"|"的时候是以特殊符号来解析,而JVM将把"|"作为普通的字符串来处理。
在通常的处理中,这种功能可以屏蔽命令溢出的漏洞,但是在特殊需求的时候,就不方便了。
当然,在特殊需求下也有解决方法,可以如下执行以下命令:
- CommandExec exec = new CommandExec();
- Process process = exec.exec(new String(){"shell", "-c", "/"find / -name '*.xml' -type f | xargs java 相应程序/""});
也就是把要执行的命令作为shell的一个参数让Runtime.exec执行,为shell命令加上-c参数就可以正常执行目标命令了。
细心的读者已经看出,用上面的方法,有一个缺点就是,如果动态生成命令,很容易发生命令溢出的漏洞。
这里有一个解决方法,就是设定一个前提,被执行命令,文字串都必须以单引号包含,那样就比较容易escape了。
escape的方法可以如下定义,并添加到CommandExec当中。
- /**
- *
- * 对shell字串进行escape
- *
- * @param s 对象字串
- * @return 返回escape之后的shell字串
- */
- public static String escapeShellSpecialCharacters(String s) {
- StringBuilder sb = new StringBuilder(s.length() + 128);
- sb.append('/'');
- for (int i = 0; s != null && i < s.length(); i++) {
- char c = s.charAt(i);
- if (c == '/'') {
- sb.append('//');
- }
- sb.append(c);
- }
- sb.append('/'');
- return sb.toString();
- }
到此为止,在Java中执行外部命令的方法以及注意点基本叙述完毕。但是,最后还有一点必须注意的就是JVM本身存在的一个漏洞。
这个漏洞的现象是,当被执行的外部命令,标准输出的量比较大的时候,程序会无故锁死。
特别是在JDK1.3版本尤为明显,在新版本中仍旧存在此漏洞。
不过,通过外部命令的执行方式,可以避免此漏洞的出现——通过重定向避免标准输出。
CommandExec的执行方法如下:
- CommandExec exec = new CommandExec();
- Process process = exec.exec(new String(){"shell", "-c", "/"find / -name '*.xml' -type f | xargs java 相应程序 > /dev/null/""});
没有评论:
发表评论