使用Java调用shell脚本时遇到的问题
最近Jackie在搞一个新项目,为了快速完成开发,需要在Java代码里使用shell脚本或者命令,便于快速完成业务需要的功能。Java SDK中关于启动进程执行外部shell命令的API很简单,很直接,所以Jackie直接参考样例就开搞了,原本以为很顺利,结果遇到了一些问题,花费了不少精力才解决。出于各方面原因,我厂的编程规范不推荐在Java代码里调用shell脚本或者命令,另外Java的开源库非常丰富,所以日常工作中几乎没有场景需要在代码中直接调用shell脚本的需求。这直接导致Jackie欠缺相关使用经验,在 简单的问题上花费了相当的时间。本文记录Jackie在相关API使用过程中遇到的问题。使用Java调用shell脚本的方法
方法一
这个方法比较直接,简单,适用于一般的场景,比如Jackie当前在做的项目。Process process = Runtime.getRuntime().exec(shellCommand);
方法二
这个方法相对要复杂一些,但对于需要向脚本传递参数的场景,会非常方便。但Jackie在项目里没有遇到类似的场景,所以没有使用这种方法。ProcessBuilder ProcessBuilder = new ProcessBuilder(shellCommand, param1, param2);
注意事项
在Java代码中调用shell脚本或者命令时,有一些小细节要注意,否则等程序运行起来,就会发现各种小问题。- shell脚本的格式需要为unix格式,如果不是的话,需要使用dos2unix或者tr等命令对脚本文件的格式做处理,删除多余的
\r
。命令的参考样例如下tr -d "\r" < winfile.sh > unixfile.sh dos2unix winfile.sh
- shell脚本或者命令需要有可执行权限,这个很好理解,不解释。
chmod +x command.sh
- shell脚本或者命令的路径。为了保证可靠性,比较省事的方法是在代码中使用脚本或者命令的全路径。在脚本里执行其它脚本或者命令时,也尽可能使用脚本或者命令的全路径,省掉准备PATH环境变量的工作。当然了,这会引入一个问题,即对脚本全路径的依赖,假如全路径是非标准路径,同时不同的设备上路径存在差异,那么Jackie推荐的做法是使用模板技术,比如Freemarker,将变化的部分抽取出来,作为变量插入到脚本里,每次执行脚本时,使用外部的变量来替换脚本中的路径,解决上述问题。但最有效、最省事的办法还是对路径进行标准化,保证脚本、命令的位置在所有设备上一致,这可以有效提高开发、运维的工作效率。
- 如需等待shell命令运行结束,获取运行结果,可以使用
Process
的waitFor
方法,样例如下:Process process = Runtime.getRuntime().exec(shellCommand); int code = process.waitFor();// waitFor方法的返回值,表示了shell命令或者脚本的返回值,方便我们的代码做进一步的处理。
- 启动shell进程后,发现进程长时间运行无法结束,同时失去响应。这个问题的原因是shell脚本或者命令在运行的过程中会向标准输出或者标准错误输出写出数据,但JVM又没有去读,导致缓冲区满,进而导致进程阻塞。这个问题的解决的方法比较简单,既然问题是缓冲区满之后没有及时清理,那么只要在Java代码里去读一下数据,保证缓冲区不会满即可。可参考如下实现。
Process process = Runtime.getRuntime().exec(shellCommand); BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())); String line; while ((line = stdout.readLine()) != null) { // do something, logging } while ((line = stderr.readLine()) != null) { // do something, logging } process.waitFor(); // 等待程序运行结束
- shell命令中如果包含了管道或者重定向的操作,在SSH终端里可以正常运行,但使用Java代码运行时会报一些奇怪的错误。这个问题让Jackie纠结了半个下午,排查了好久,后来在网上搜索了一些文章,终于确认是使用方法的问题。问题代码
正确的代码Process process = Runtime.getRuntime().exec("tr -d '\r' < run.sh.template > run.sh");
差别在于带有管道或者重定向操作的命令需要作为参数传递给命令Process process = Runtime.getRuntime().exec("/bin/sh", "-c", "tr -d '\r' < run.sh.template > run.sh");
/bin/sh
方能正常执行,否则就会报各种莫明其妙的错误,让人百思不得其解。 - 其它问题。等遇到了再来总结吧。
参考资料
网上关于Java代码中调用shell脚本或者命令的文章非常多,Jackie只是摘录了一部分。- JAVA调用Shell脚本
- java调用shell命令并获取执行结果
- Java Process中waitFor()的问题
- 怎么通过java去调用并执行shell脚本以及问题总结
- Java Runtime.exec()的使用
- JAVA中Runtime类以及exec()方法,Process的使用