背景

之前写脚本都是用linux shell或者python,非常方便。但是服务器上生产环境中的python依赖较少,无法开发功能强大的应用,因此转战java开发。在java中执行shell脚本,其实就是在jvm进程中创建子进程运行shell脚本,为了读取shell脚本输出,或者向shell脚本发送信息,这必须包含进程间的通信。

原理

  1. 父进程退出时,子进程还没结束,子进程成为孤儿进程,被托管到系统父进程下。尽量保证父进程退出前,子进程退出。
  2. Java可以通过Runtime.getRuntime().exec()执行命令,它其实是在jvm进程中开启一个子进程。父进程和子进程之间通过管道通信。

常见错误

public class CommandTest {

    @Test
    public void testExec() throws IOException, InterruptedException {

        Process pro = Runtime.getRuntime().exec(CommandTest.class.getResource("/script/command.sh").getPath());
        int status = pro.waitFor();
        if (status != 0) {
            System.out.println(status);
            System.out.println("Failed to call shell's command ");
        }
        BufferedReader br = new BufferedReader(new InputStreamReader(pro.getInputStream()));
        StringBuffer strbr = new StringBuffer();
        String line;
        while ((line = br.readLine()) != null) {
            strbr.append(line).append("\n");
        }
        String result = strbr.toString();
        System.out.println(result);
    }
}

上述代码中,为了在主进程退出前保证子进程退出,调用了pro.waitFor()阻塞主进程。这导致两个进程发生死锁,即主进程和子进程都卡住了。这是因为子进程将执行结果发送到管道中等待主进程消费,在管道缓存满后,主进程消费前,子进程会阻塞,直到管道的内容被主进程消费;此时由于pro.waitFor()把主进程阻塞,导致两个进程都阻塞了,因此发生死锁。

改进

在主进程消费完队列后,再调用pro.waitFor()等待子进程退出,并调用pro.destroy()关闭子进程,不会发生死锁。

public class CommandTest {
    private CommandExecutor executor = new CommandExecutor();

    @Test
    public void testExec() throws IOException, InterruptedException {

        Process pro = Runtime.getRuntime().exec(CommandTest.class.getResource("/script/command.sh").getPath());
        BufferedReader br = new BufferedReader(new InputStreamReader(pro.getInputStream()));
        StringBuffer strbr = new StringBuffer();
        String line;
        while ((line = br.readLine()) != null) {
            strbr.append(line).append("\n");
        }
        pro.waitFor();
        pro.destroy();
        String result = strbr.toString();
        System.out.println(result);
    }
}

扩展

Java执行shell命令方式常有三种:

  1. 执行无参脚本 脚本在项目的resources目录下,直接将文件全路径名作为Runtime.getRuntime().exec()参数即可,如下:
// CommandTest为当前类
Process pro = Runtime.getRuntime().exec(CommandTest.class.getResource("/script/command.sh").getPath());

其中,CommandTest.class.getResource("/script/command.sh").getPath()用于通过classpath路径获取resources目录下/script/command.sh文件在文件系统中的绝对路径。

  1. 执行有参脚本 将执行脚本的命令、脚本路径和参数封装到一个数组,并传给exec()中。数组一般由三个字符串组成:"/bin/bash"(shell命令)、"-c"(参数)、"script parm ..."(脚本及其参数)
# script表示classpath下脚本路径,args为输入给脚本的参数
public List<String> execScript(String script, String[] args) {
    //获取绝对路径,AbstractCommandExecutor为当前类
    String absPath = AbstractCommandExecutor.class.getResource(script).getPath();
    //脚本文件及其参数应该在一个字符串中,以空格进行间隔
    String paraScript = absPath + " " + String.join(" ", args)
	//调用bash命令执行脚本
    Process chiProcess = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", paraScript});
}

脚本:

#!/bin/bash
rsync -r -e 'ssh' $1@$2:/home/myspace/mydic $3
  1. 执行命令行 直接执行将命令作为exec参数即可:
String command = "ls -al";
Process chiProcess = Runtime.getRuntime().exec(command);