Java调用Shell命令

项目中经常会出现一些场景,需要我们通过Java程序去调用Shell命令,比如获取服务器的某些信息、调用PHP脚本、调用某个软件等等。
原理
1、可以通过如下两种方式,在主进程中创建一个子进程来执行Shell脚本,这两种方式都会返回一个Process对象,用于获取Shell脚本的输出结果:
1、Runtime.exec()
2、ProcessBuilder.start()
2、主进程通过Process.waitfo()等待脚本执行完成
3、脚本执行完成,子进程退出
4、主进程通过Process.getInputStream()和Process.getErrorStream()对脚本返回值或者输出结果进行处理
常见问题
Process.waitfor()导致进程挂起
输出(标准输出&错误输出)要用单独的线程来处理,否则会卡住,原因如下(参考这篇博客,里面从原理上进行了详细讲解):
主进程中调用Runtime.exec会创建一个子进程,用于执行shell脚本。子进程创建后会和主进程分别独立运行。
因为主进程需要等待脚本执行完成,然后对脚本返回值或输出进行处理,所以这里主进程调用Process.waitfor等待子进程完成。
通过shell脚本可以看出:子进程执行过程就是不断的打印信息。主进程中可以通过Process.getInputStream和Process.getErrorStream获取并处理。
这时候子进程不断向主进程发生数据,而主进程调用Process.waitfor后已挂起。
当前子进程和主进程之间的缓冲区塞满后,子进程不能继续写数据,然后也会挂起。这样子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致
死锁。
基于上述分析,只要主进程在waitfor之前,能不断处理缓冲区中的数据就可以。
因为,我们可以在waitfor之前,单独启两个额外的线程,分别用于处理InputStream和ErrorStream即可。
无法执行管道命令
执行管道命令,需要将命令拆分为3部分,类似这样:
String cmd = "ps aux|grep php";
String[] cmds = {"/bin/sh", "-c", command};
final Process p = Runtime.getRuntime().exec(cmds);
参数过长(Argument list too long)
可以看看是否可以将参数拆分为多次执行。
比如我的应用场景是需要将一段很长的文本从简体中文转为繁体中文,那么我可以尝试将这个文本以10W长度为一个单位进行拆分,然后多次调用Shell命令进行转换。
注意,这个方法有个弊端:因为参数拆分后,命令会被执行多次,相应的处理时间也会增加N倍。因此这个方案要看具体场景。如果是对于性能要求不高的后端任务是没有问题的;如果是直接面向用户的HTTP服务,那么这个方案就不可取了。
参数中包含空格或者引号、$等特殊字符
对这些特殊符号进行转义,然后在参数首尾加上引号”,将整个参数包装为一个字符串即可。
处理输出结果的线程里面的数据如何传递到外层?
可以采用观察者模式,在线程类中,将雇主类的引用设置进去。
参考这个文章:https://blog.csdn.net/yumeizui8923/article/details/79409668
Java调用Shell命令,性能很差
经测试,通过Java调用Shell,和直接命令行手动执行Shell命令,性能相差10倍。
这个问题尚待排查和解决。
代码
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class OpenCC {
// 经测试,15W会报:Argument list too long
private static int words = 100000;
private ArrayList<String> timer = new ArrayList<String>();
// 不能用字符串直接拼接,否则性能极差
private StringBuffer result = new StringBuffer("");
public void appendResult(String content) {
this.result.append(content);
}
public String getResult() {
return this.result.toString();
}
public void log(String msg)
{
this.timer.add(System.currentTimeMillis() + "\t:\t" + msg);
}
public void showPerformance()
{
for (String msg : this.timer) {
System.out.println(msg);
}
}
public String sc2twp(String content)
{
this.log("进入sc2twp");
StringBuffer sb = new StringBuffer("");
content = content.replace("\"", "\\\"");
// 这个会被识别为shell的变量符号
content = content.replace("$", "dollar");
int length = content.length();
int count = length / this.words;
if (0 == count) {
sb.append(this.transfer(content));
return sb.toString();
}
for (int i = 0; i < count; i ++) {
String temp = content.substring(i * this.words, (i + 1) * this.words);
sb.append(this.transfer(temp));
}
return sb.toString();
}
/**
* 执行命令
*
* @return
*/
public String transfer(String content) {
this.log("开始转换");
// 这里文本太长了不行,得分批处理
String command = "echo \"" + content + "\" |/usr/bin/opencc -c s2twp";
String[] cmds = {"/bin/sh", "-c", command};
this.log("解析完命令");
final String resultLog = "";
try {
this.log("before Runtime.getRuntime().exec");
// DEBUG:这个方法耗时很长,即使一个简单的字符串转换,也要耗费100多毫秒
final Process p = Runtime.getRuntime().exec(cmds);
this.log("开启进程");
InputStream is = p.getInputStream();
BufferedReader bs = new BufferedReader(new InputStreamReader(is));
//处理Stream的线程
InputStreamThread ist = new InputStreamThread();
ist.setOpenCC(this);
ist.setInputStream(p.getInputStream());
ist.start();
InputStreamThread est = new InputStreamThread();
est.setOpenCC(this);
est.setInputStream(p.getErrorStream());
est.start();
this.log("before wait");
try {
p.waitFor();
this.log("after wait");
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
return null;
}
if (p.exitValue() != 0) {
//说明命令执行失败
//可以进入到错误处理步骤中
System.out.println("Warning!Execute failed ");
}
} catch (IOException ioException) {
ioException.printStackTrace();
return null;
}
return resultLog;
}
/**
* 将整个文件的内容读入一个字符串变量
*
* @param filePath
* @return
* @throws IOException
*/
public String read(String filePath) throws IOException {
StringBuilder sb = new StringBuilder();
String str;
BufferedReader br = new BufferedReader(new FileReader(filePath));
while (null != (str = br.readLine())) {
sb.append(str + "\n");
}
br.close();
return sb.toString();
}
public static void main(String[] args) {
OpenCC openCC = new OpenCC();
String content = "我是一名软件工程师,我喜欢鼠标键盘和服务器";
// try {
// content = openCC.read("home.html");
// } catch (IOException ioExp) {
// ioExp.printStackTrace();
// System.exit(0);
// }
openCC.sc2twp(content);
String tw = openCC.getResult();
openCC.showPerformance();
System.out.println(tw);
}
}
/**
* 处理输出流
*/
class InputStreamThread extends Thread {
private OpenCC openCC;
private InputStream inputStream;
public void setOpenCC(OpenCC openCC) {
this.openCC = openCC;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
BufferedReader in = new BufferedReader(new InputStreamReader(this.inputStream));
String line = null;
this.openCC.log("Thread 处理结果之前");
try {
while ((line = in.readLine()) != null) {
// 这里记得加换行符,否则类似js、css这种代码会出问题
this.openCC.appendResult(line + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
this.openCC.log("Thread 结果处理完成");
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}