记一次Java调用系统命令错误总结

最近在做一个项目,经常需要写 bat 脚本,后来由于需要把bat脚本的内容内嵌代码中,从而引发了一些问题。

执行启动Mysql命令

问题:在 Java 中执行 C:\Program Files\MySQL\MySQL Server 5.1\bin\mysqld --defaults-file=C:\Program Files\MySQL\MySQL Server 5.1\bin\my.ini 这条命令? 初次看到这条命令时,很自然的就想到了以下的方式:

@Test
public void cmdTest1() {
    String sCommand = "\"C:\\Program Files\\MySQL\\MySQL Server 5.1\\bin\\mysqld\" --defaults-file=\"C:\\Program Files\\MySQL\\MySQL Server 5.1\\bin\\my.ini\""; 
    try {
		Runtime.getRuntime().exec(new String[]{"cmd","/c",sCommand});
	} catch (IOException e) {
		System.out.println("command :"+cmd+" "+e.getMessage());
	}
}

这段代码从实现上看没有什么大的问题,然而在实际运行中却无法启动 Mysql 。那么这是什么原因呢?

探索原因

这还要从 JDK 源码中找到答案。本文以 jdk1.7.0_75 源码为例,不同 JDK 源码可能存在差异。首先,我们看以下部分JDK代码(本部分代码来自 Runtime 类中的 exec 方法):

public Process exec(String command, String[] envp, File dir)
        throws IOException {
        if (command.length() == 0)
            throw new IllegalArgumentException("Empty command");

        StringTokenizer st = new StringTokenizer(command);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++)
            cmdarray[i] = st.nextToken();
        return exec(cmdarray, envp, dir);
    }

上述代码是将传入的命令根据空格进行分割,然后放入 cmdarray 数组中。这个数组内容是{"D:\Program,Files\MySQL\MySQL,Server,5.1\bin\mysqldUSB.exe",--defaults-file="D:\Program,Files\MySQL\MySQL,Server,5.1\my.ini"}。而后,我们继续阅读代码在 java.lang.ProcessImpl 类中找到了我们需要的答案。

private static boolean needsEscaping(int verificationType, String arg) {
        boolean argIsQuoted = isQuoted(
            (verificationType == VERIFICATION_CMD_BAT),
            arg, "Argument has embedded quote, use the explicit CMD.EXE call.");

        if (!argIsQuoted) {
            char testEscape[] = ESCAPE_VERIFICATION[verificationType];
            for (int i = 0; i < testEscape.length; ++i) {
                if (arg.indexOf(testEscape[i]) >= 0) {
                    return true;
                }
            }
        }
        return false;
    }

arg 是前面的 cmdarray 数组元素,这里的 verificationType 值是2,取出的 ESCAPE_VERIFICATION 数组内容是 {' ', '\t'} 。所以这段代码是判断 arg 中是否含有 空格或者制表符。接着我们来看调用这个函数的地方:

private static String createCommandLine(int verificationType,
                                     final String executablePath,
                                     final String cmd[])
    {
        StringBuilder cmdbuf = new StringBuilder(80);

        cmdbuf.append(executablePath);

        for (int i = 1; i < cmd.length; ++i) {
            cmdbuf.append(' ');
            String s = cmd[i];
            if (needsEscaping(verificationType, s)) {
                cmdbuf.append('"').append(s);

                // The code protects the [java.exe] and console command line
                // parser, that interprets the [\"] combination as an escape
                // sequence for the ["] char.
                //     http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
                //
                // If the argument is an FS path, doubling of the tail [\]
                // char is not a problem for non-console applications.
                //
                // The [\"] sequence is not an escape sequence for the [cmd.exe]
                // command line parser. The case of the [""] tail escape
                // sequence could not be realized due to the argument validation
                // procedure.
                if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) {
                    cmdbuf.append('\\');
                }
                cmdbuf.append('"');
            } else {
                cmdbuf.append(s);
            }
        }
        return cmdbuf.toString();
    }

从上述代码可以看出: cmdarray 数组中的元素若含有空格或者制表符则在这个数组元素的前后加上引号"",然后将每一个元素重新拼接。再来看我们传入的命令 C:\Program Files\MySQL\MySQL Server 5.1\bin\mysqld --defaults-file=C:\Program Files\MySQL\MySQL Server 5.1\bin\my.ini 。居然在路径中带有空格,所以在执行代码的时候由于带有空格导致实际传入mysqld的路径是错的,,真实传入的路径就变成了C:\ProgramFiles\MySQL\MySQLServer5.1\bin,那么肯定就无法启动mysql咯。

解决方法

那么,我们要如何做才能启动mysql呢? 从API中我们可以看到Runtime的exec方法重载了好几个。其中有这么一个方法: public Process exec(String command, String[] envp, File dir) 。参数分别代表的是:

  1. command : 要执行的命令
  2. envp : 指定需要的环境变量
  3. dir : 命令执行的子目录

代码实现如下:

@Test
public void cmdTest2() {
    String sDir = "C:\\Program Files\\MySQL\\MySQL Server 5.1\\bin";
    String sStartMysql =  "mysqldUSB --defaults-file=../my.ini";
    try {
		Runtime.getRuntime().exec(new String[]{"cmd","/c",sStartMysql}, null, new File(sDir));
	} catch (IOException e) {
		System.out.println("command :"+cmd+" "+e.getMessage());
	}
}

Java执行多条命令

将 cmd 脚本文件写入代码中,不可避免的会遇到一次执行多条命令的情况。但是 exec 方法一次只能执行一条命令。

利用脚本本身的特点

我们可以利用脚本本身的一些特点来实现执行多条命令。(这里以 cmd 命令为例) 主要利用 & 和 && 字符的特性。它们的区别如下:

  1. & 连接多条命令,且不关系前一条是否执行成功;
  2. && 连接多条命令,前一条命令执行失败后不再执行后续命令。

利用流实现

执行 exec 方法后会返回一个 Process 对象。 Process 类中有一个方法是 getOutputStream ,用于获取子进程的输出流。输出流被传送给由该 Process 对象表示的进程的标准输入流。根据这一特性,我们可以执行多条命令。

@Test
public void cmdTest3() {
    //本次代码省略具体的命令集 sCommands 内容
    DataOutputStream out=null;
	try {
		Process process = Runtime.getRuntime().exec("cmd");
		out = new DataOutputStream(process.getOutputStream());
		for(String sCommand: sCommands) {
			out.writeBytes(sCommand);
			out.writeBytes("\n");
			out.flush();
		}
		out.writeBytes("exit\n");
		out.flush();
	} catch (IOException e) {
		System.err.println(e.getMessage());
	} finally {
		try {
			if(out != null) out.close();
		} catch (IOException e) {
			System.err.println(e.getMessage());
		}
	}
}

通过流的方式我们将每一条命令传送过去执行。要注意的是执行结束后需要执行 exit 命令用于关闭 cmd 。

总结

通过这次的教训深深感受到需要加深对 JDK 源码的理解,一段看似合理的启动 Mysql 代码却由于对 JDK 源码的不熟悉而导致执行失败,并花费了比较长的时间排查问题。同时,还需要多多查阅 API ,一些不常用的 API 可能就会帮助我们解决一个困扰已久或者实行复杂的问题。

转载于:https://my.oschina.net/ptczy/blog/1631347

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值