记一次Runtime.exec遇到的坑

首先来看一段代码(Java17, Java8同样也有这个问题):

Runtime runtime = Runtime.getRuntime(); runtime.exec("docker exec mysql-test2 mysql -ubim -pxx -h11.11.11.11 -P3306 bim -e \"use bim;source bim.sql;\"");
java

这段代码是用来执行mysqldump备份出来的mysql文件,咋一看好像没问题。

运行后,byd好像也确实没什么问题👿👿👿

如果真的没问题就好了,那么也不会有这篇博客了。

写完后打包丢到Linux上去跑的时候,你就会发现。。。

运行后发现执行失败了,exitCode为1,看了一下它的输出,它居然直接把mysql的帮助菜单给打印出来了???

见过离谱的,没见过这么离谱的,我这个指令可是跟帮助菜单一点关系都没有啊?


如果大家去搜java怎么去执行命令行指令的时候,可能会得到两个结果,一种就是用Runtime,另外一个就是用ProcessBuilder,大部分人可能都会用Runtime,因为这玩意给ProcessBuilder封装了一层,用起来方便,直接一个exec把指令丢进去就可以了。

也就是因为这个,突然想起来之前在python里面,想要执行shell命令必须要把指令以数组的形式传进去(其实也可以使用shell=True参数),而ProcessBuilder也是这样,你直接丢一个字符串进去是执行不了的,必须要传数组进去。

到这里就怀疑Runtime是不是直接暴力调用了split(" "),然后把参数丢给ProcessBuilder,结果看了下源码,还真是这样:

public Process exec(String command, String[] envp, File dir) throws IOException { if (command.isEmpty()) 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); }
java

StringTokenizer可能大家没见过,但是如果你用java写算法,并且了解过输入优化,你就会知到这玩意是干嘛的。简单点来说它的效果和Scanner一样,但是效率更高(如果你写过算法就知道这玩意速度吊打Scanner),Scanner就不多讲了,感觉是个人就用过。。。

如果你还看不懂,没事,我直接给你上图: debug

可以发现我们后面用双引号包裹起来的参数被分开了,实际传到mysql那里就会导致执行失败。

但是这玩意在windows上能执行成功也是很离谱的。

知到原因后,直接改用ProcessBuilder手动控制参数:

Process process = new ProcessBuilder(backupConfig.getMysqlPath(), "-u" + backupConfig.getUsername(), "-p" + backupConfig.getPassword(), "-h" + backupConfig.getHost(), "-P" + backupConfig.getPort(), "-e", String.format("\"use %s;source %s;\"", ignore, ignore)) // 标准错误流重定向到标准输出,方便拿错误信息 .redirectErrorStream(true) .start();
java

看到我的String.format没,我这里用引号包起来了好让他们是一个整体。

完?。。


丢到服务器上跑,结果又报错了👿,不过至少这次没打帮助菜单,提示""use %s;source %s;""不是一个mysql指令。

byd原来引号是自作多情多加上了,最后把引号给删掉就能跑起来了。。

这bug也是花了我挺多时间的吧,一开始以为是mysql的问题,结果居然是jdk自己的问题。