Java 调用 shell 脚本详解

Posted hyl8218

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 调用 shell 脚本详解相关的知识,希望对你有一定的参考价值。

这一年的项目中,有大量的场景需要Java 进程调用 Linux的bash shell 脚本实现相关功能。

从之前的项目中拷贝的相关模块和网上的例子来看,有个别的“陷阱”造成调用shell 脚本在某些特殊的场景下,有一些奇奇怪怪的bug。

 

大家且听我一一道来。

 

先看看网上搜索到的例子:

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStreamReader;  
  6.   
  7. public class ShellTest {  
  8.   
  9.     public static void main(String[] args) {  
  10.         InputStreamReader stdISR = null;  
  11.         InputStreamReader errISR = null;  
  12.         Process process = null;  
  13.         String command = "/home/Lance/workspace/someTest/testbash.sh";  
  14.         try {  
  15.             process = Runtime.getRuntime().exec(command);  
  16.             int exitValue = process.waitFor();  
  17.   
  18.             String line = null;  
  19.   
  20.             stdISR = new InputStreamReader(process.getInputStream());  
  21.             BufferedReader stdBR = new BufferedReader(stdISR);  
  22.             while ((line = stdBR.readLine()) != null) {  
  23.                 System.out.println("STD line:" + line);  
  24.             }  
  25.   
  26.             errISR = new InputStreamReader(process.getErrorStream());  
  27.             BufferedReader errBR = new BufferedReader(errISR);  
  28.             while ((line = errBR.readLine()) != null) {  
  29.                 System.out.println("ERR line:" + line);  
  30.             }  
  31.         } catch (IOException | InterruptedException e) {  
  32.             e.printStackTrace();  
  33.         } finally {  
  34.             try {  
  35.                 if (stdISR != null) {  
  36.                     stdISR.close();  
  37.                 }  
  38.                 if (errISR != null) {  
  39.                     errISR.close();  
  40.                 }  
  41.                 if (process != null) {  
  42.                     process.destroy();  
  43.                 }  
  44.             } catch (IOException e) {  
  45.                 System.out.println("正式执行命令:" + command + "有IO异常");  
  46.             }  
  47.         }  
  48.     }  
  49. }  

 

 

testbash.sh

 

[plain] view plain copy
 
  1. #!/bin/bash  
  2.   
  3. echo `pwd`  

 

 

输出结果为:

 

[plain] view plain copy
 
  1. STD line:/home/Lance/workspace/someTest  


Java在执行Runtime.getRuntime().exec(command)之后,Linux会创建一个进程,该进程与JVM进程建立三个管道连接,标准输入流、标准输出流、标准错误流。

 

上述代码,依次读取标准输出流和标准错误流,在shell给出“退出信号”后,做了相应的清理工作。

 

对于一般场景来说,这段代码可以凑合用了。但是,在实际场景中,会有以下几个“陷阱”。

 

一. 当标准输出流或标准错误流非常庞大的时候,会出现调用waitFor方法卡死的bug。

真实的环境中,当标准输出在10000行左右的时候,就会出现卡死的情况。

 

原因分析:假设linux进程不断向标准输出流和标准错误流写数据,而JVM却不读取,数据会暂存在linux缓存区,当缓存区存满之后导致该进程无法继续写数据,会僵死,导致java进程会卡死在waitFor()处,永远无法结束。

 

解决方式:由于标准输出和错误输出都会向Linux缓存区写数据,而脚本如何输出这两种流是Java端不能确定的。为了不让shell脚本的子进程卡死,这两种输出需要分别读取,而且不能互相影响。所以必须新开两个线程来进行读取。

 

我开始的实现如下:

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStream;  
  6. import java.io.InputStreamReader;  
  7. import java.util.LinkedList;  
  8. import java.util.List;  
  9.   
  10. public class CommandStreamGobbler extends Thread {  
  11.   
  12.     private InputStream is;  
  13.   
  14.     private String command;  
  15.   
  16.     private String prefix = "";  
  17.   
  18.     private boolean readFinish = false;  
  19.   
  20.     private boolean ready = false;  
  21.   
  22.     private List<String> infoList = new LinkedList<String>();  
  23.   
  24.     CommandStreamGobbler(InputStream is, String command, String prefix) {  
  25.         this.is = is;  
  26.         this.command = command;  
  27.         this.prefix = prefix;  
  28.     }  
  29.   
  30.     public void run() {  
  31.         InputStreamReader isr = null;  
  32.         try {  
  33.             isr = new InputStreamReader(is);  
  34.             BufferedReader br = new BufferedReader(isr);  
  35.             String line = null;  
  36.             ready = true;  
  37.             while ((line = br.readLine()) != null) {  
  38.                 infoList.add(line);  
  39.                 System.out.println(prefix + " line: " + line);  
  40.             }  
  41.         } catch (IOException ioe) {  
  42.             System.out.println("正式执行命令:" + command + "有IO异常");  
  43.         } finally {  
  44.             try {  
  45.                 if (isr != null) {  
  46.                     isr.close();  
  47.                 }  
  48.             } catch (IOException ioe) {  
  49.                 System.out.println("正式执行命令:" + command + "有IO异常");  
  50.             }  
  51.             readFinish = true;  
  52.         }  
  53.     }  
  54.   
  55.     public InputStream getIs() {  
  56.         return is;  
  57.     }  
  58.   
  59.     public String getCommand() {  
  60.         return command;  
  61.     }  
  62.   
  63.     public boolean isReadFinish() {  
  64.         return readFinish;  
  65.     }  
  66.   
  67.     public boolean isReady() {  
  68.         return ready;  
  69.     }  
  70.   
  71.     public List<String> getInfoList() {  
  72.         return infoList;  
  73.     }  
  74. }  

 

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStreamReader;  
  5.   
  6. public class ShellTest {  
  7.   
  8.     public static void main(String[] args) {  
  9.         InputStreamReader stdISR = null;  
  10.         InputStreamReader errISR = null;  
  11.         Process process = null;  
  12.         String command = "/home/Lance/workspace/someTest/testbash.sh";  
  13.         try {  
  14.             process = Runtime.getRuntime().exec(command);  
  15.   
  16.             CommandStreamGobbler errorGobbler = new CommandStreamGobbler(process.getErrorStream(), command, "ERR");  
  17.             CommandStreamGobbler outputGobbler = new CommandStreamGobbler(process.getInputStream(), command, "STD");  
  18.   
  19.             errorGobbler.start();  
  20.             // 必须先等待错误输出ready再建立标准输出  
  21.             while (!errorGobbler.isReady()) {  
  22.                 Thread.sleep(10);  
  23.             }  
  24.             outputGobbler.start();  
  25.             while (!outputGobbler.isReady()) {  
  26.                 Thread.sleep(10);  
  27.             }  
  28.   
  29.             int exitValue = process.waitFor();  
  30.         } catch (IOException | InterruptedException e) {  
  31.             e.printStackTrace();  
  32.         } finally {  
  33.             try {  
  34.                 if (stdISR != null) {  
  35.                     stdISR.close();  
  36.                 }  
  37.                 if (errISR != null) {  
  38.                     errISR.close();  
  39.                 }  
  40.                 if (process != null) {  
  41.                     process.destroy();  
  42.                 }  
  43.             } catch (IOException e) {  
  44.                 System.out.println("正式执行命令:" + command + "有IO异常");  
  45.             }  
  46.         }  
  47.     }  
  48. }  


到此为止,解决了Java卡死shell脚本的情况。再说说,第二种可能。

 

 

二. 由于shell脚本的编写问题,当其自身出现僵死的情况,上述代码出现Java代码被僵死的Shell脚本阻塞住的情况。

 

原因分析:由于shell脚本也是人写的,难免会出现失误。在Java调用shell脚本时,无论是Debug场景还是生产环境,都发生过shell脚本意外僵死反过来卡死Java相关线程的情况。典型的表现为:shell脚本长时间运行,标准输出和错误输出没有任何输出(包括结束符),操作系统显示shell脚本在正常运行或僵死,没有退出信号。

解决方式:上述代码中,至少有三处会导致线程阻塞,包括标准输出和错误输出这线程的BufferedReader的readline方法,以及Process的waitFor方法。解决这个问题的核心有两个,1.避免任何Java线程被阻塞住,因为一旦被IO阻塞住,线程将处于内核态,主线程没有任何办法强制结束相关子线程。2.添加一个简单的超时机制,超时后回收相应的线程资源,并结束调用过程。

 

演示代码中,我改写了testshell.sh,写一个没有任何输出的死循环模拟shell卡死的情况。

 

[plain] view plain copy
 
  1. #!/bin/bash  
  2.   
  3. while true;do   
  4.     a=1  
  5.     sleep 0.1  
  6. done  

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStream;  
  6. import java.io.InputStreamReader;  
  7. import java.util.LinkedList;  
  8. import java.util.List;  
  9.   
  10. public class CommandStreamGobbler extends Thread {  
  11.   
  12.     private InputStream is;  
  13.   
  14.     private String command;  
  15.   
  16.     private String prefix = "";  
  17.   
  18.     private boolean readFinish = false;  
  19.   
  20.     private boolean ready = false;  
  21.   
  22.         // 命令执行结果,0:执行中 1:超时 2:执行完成  
  23.         private int commandResult = 0;  
  24.   
  25.     private List<String> infoList = new LinkedList<String>();  
  26.   
  27.     CommandStreamGobbler(InputStream is, String command, String prefix) {  
  28.         this.is = is;  
  29.         this.command = command;  
  30.         this.prefix = prefix;  
  31.     }  
  32.   
  33.     public void run() {  
  34.         InputStreamReader isr = null;  
  35.         BufferedReader br = null;  
  36.         try {  
  37.             isr = new InputStreamReader(is);  
  38.             br = new BufferedReader(isr);  
  39.             String line = null;  
  40.             ready = true;  
  41.             while (commandResult != 1) {  
  42.                 if (br.ready() || commandResult == 2) {  
  43.                                     if ((line = br.readLine()) != null) {  
  44.                                         infoList.add(line);  
  45.                                     } else {  
  46.                                         break;  
  47.                                     }  
  48.                                 } else {  
  49.                                     Thread.sleep(100);  
  50.                                 }  
  51.             }  
  52.         } catch (IOException | InterruptedException ioe) {  
  53.             System.out.println("正式执行命令:" + command + "有IO异常");  
  54.         } finally {  
  55.             try {  
  56.                 if (br != null) {  
  57.                     br.close();  
  58.                 }  
  59.                 if (isr != null) {  
  60.                     isr.close();  
  61.                 }  
  62.             } catch (IOException ioe) {  
  63.                 System.out.println("正式执行命令:" + command + "有IO异常");  
  64.             }  
  65.             readFinish = true;  
  66.         }  
  67.     }  
  68.   
  69.     public InputStream getIs() {  
  70.         return is;  
  71.     }  
  72.   
  73.     public String getCommand() {  
  74.         return command;  
  75.     }  
  76.   
  77.     public boolean isReadFinish() {  
  78.         return readFinish;  
  79.     }  
  80.   
  81.     public boolean isReady() {  
  82.         return ready;  
  83.     }  
  84.   
  85.     public List<String> getInfoList() {  
  86.         return infoList;  
  87.     }  
  88.   
  89.     public void setTimeout(int timeout) {  
  90.         this.commandResult = timeout;  
  91.     }  
  92. }  

 

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. public class CommandWaitForThread extends Thread {  
  4.   
  5.     private Process process;  
  6.     private boolean finish = false;  
  7.     private int exitValue = -1;  
  8.   
  9.     public CommandWaitForThread(Process process) {  
  10.         this.process = process;  
  11.     }  
  12.   
  13.     public void run() {  
  14.         try {  
  15.             this.exitValue = process.waitFor();  
  16.         } catch (InterruptedException e) {  
  17.             e.printStackTrace();  
  18.         } finally {  
  19.             finish = true;  
  20.         }  
  21.     }  
  22.   
  23.     public boolean isFinish() {  
  24.         return finish;  
  25.     }  
  26.   
  27.     public void setFinish(boolean finish) {  
  28.         this.finish = finish;  
  29.     }  
  30.   
  31.     public int getExitValue() {  
  32.         return exitValue;  
  33.     }  
  34.   
  35. }  

 

 

 

[java] view plain copy
 
  1. package someTest;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStreamReader;  
  5. import java.util.Date;  
  6.   
  7. public class ShellTest {  
  8.   
  9.     public static void main(String[] args) {  
  10.         InputStreamReader stdISR = null;  
  11.         InputStreamReader errISR = null;  
  12.         Process process = null;  
  13.         String command = "/home/Lance/workspace/someTest/testbash.sh";  
  14.         long timeout = 10 * 1000;  
  15.         try {  
  16.             process = Runtime.getRuntime().exec(command);  
  17.   
  18.             CommandStreamGobbler errorGobbler = new CommandStreamGobbler(process.getErrorStream(), command, "ERR");  
  19.             CommandStreamGobbler outputGobbler = new CommandStreamGobbler(process.getInputStream(), command, "STD");  
  20.   
  21.             errorGobbler.start();  
  22.             // 必须先等待错误输出ready再建立标准输出  
  23.             while (!errorGobbler.isReady()) {  
  24.                 Thread.sleep(10);  
  25.             }  
  26.             outputGobbler.start();  
  27.             while (!outputGobbler.isReady()) {  
  28.                 Thread.sleep(10);  
  29.             }  
  30.   
  31.             CommandWaitForThread commandThread = new CommandWaitForThread(process);  
  32.             commandThread.start();  
  33.   
  34.             long commandTime = new Date().getTime();  
  35.             long nowTime = new Date().getTime();  
  36.             boolean timeoutFlag = false;  
  37.             while (!commandIsFinish(commandThread, errorGobbler, outputGobbler)) {  
  38.                 if (nowTime - commandTime > timeout) {  
  39.                     timeoutFlag = true;  
  40.                     break;  
  41.                 } else {  
  42.                     Thread.sleep(100);  
  43.                     nowTime = new Date().getTime();  
  44.                 }  
  45.             }  
  46.             if (timeoutFlag) {  
  47.                 // 命令超时  
  48.                 errorGobbler.setTimeout(1);  
  49.                 outputGobbler.setTimeout(1);  
  50.                 System.out.println("正式执行命令:" + command + "超时");  
  51.             }else {  
  52.                 // 命令执行完成  
  53.                 errorGobbler.setTimeout(2);  
  54.                 outputGobbler.setTimeout(2);  
  55.                         }  
  56.   
  57.             while (true) {  
  58.                 if (errorGobbler.isReadFinish() && outputGobbler.isReadFinish()) {  
  59.                     break;  
  60.                 }  
  61.                 Thread.sleep(10);  
  62.             }  
  63.         } catch (IOException | InterruptedException e) {  
  64.             e.printStackTrace();  
  65.         } finally {  
  66.             if (process != null) {  
  67.                 process.destroy();  
  68.             }  
  69.         }  
  70.        }  
  71.   
  72.     private boolean commandIsFinish(CommandWaitForThread commandThread, CommandStreamGobbler errorGobbler, CommandStreamGobbler outputGobbler) {  
  73.         if (commandThread != null) {  
  74.             return commandThread.isFinish();  
  75.         } else {  
  76.             return (errorGobbler.isReadFinish() && outputGobbler.isReadFinish());  
  77.         }  
  78.     }  
  79. }  

在以上的代码中,为了防止线程被阻塞,要点如下:

 

1. 在CommandStreamGobbler里,bufferedReader在readLine()之前,先用ready()看一下当前缓冲区的情况,请特别注意ready()描述,这个方法是非阻塞的。

 

[java] view plain copy
 
  1. boolean java.io.BufferedReader.ready() throws IOException  
  2.   
  3. Tells whether this stream is ready to be read. A buffered character stream is ready if the buffer is not empty, or if the underlying character stream is ready.  
  4.   
  5. Returns:  
  6. True if the next read() is guaranteed not to block for input, false otherwise. Note that returning false does not guarantee that the next read will block.  

 

 

2.在一个新线程commandThread中,调用process对象的waitFor()从而避免主线程卡死,主线程的最后会执行finally块中的process.destory()保证commandThread正常退出。

 

以上的两点改进,保证了Java在调用shell脚本过程互不被对方卡死的机制。

 

三.在执行shell脚本过程中,可能会添加参数,通常在终端中,我们使用“ ”(空格)把参数隔开。

为了区分空格是作为参数分隔符,还是参数的一部分。调用exec方法有特别的注意事项。

 

 

[java] view plain copy
 
  1. String command = "/home/Lance/workspace/someTest/testbash.sh ‘hello world‘";  
  2.   
  3. process = Runtime.getRuntime().exec(command);  


等价于

 

 

[java] view plain copy
 
  1. List<String> commandList = new LinkedList<String>();  
  2. commandList.add("/home/Lance/workspace/someTest/testbash.sh");  
  3. commandList.add("hello world");  
  4. String[] commands = new String[commandList.size()];  
  5. for (int i = 0; i < commandList.size(); i++) {  
  6.     commands[i] = commandList.get(i);  
  7. }  
  8.   
  9. process = Runtime.getRuntime().exec(commands);  

 

 

好了,今天介绍到这里。




以上是关于Java 调用 shell 脚本详解的主要内容,如果未能解决你的问题,请参考以下文章

怎么用java代码调用远程Linux上的shell脚本

java调用shell脚本,并得到shell脚本的返回值

shell脚本怎么调用其他shell脚本

windows下java怎样调用shell脚本文件

调用shell脚本 怎么像调用java方法那样传入参数

shell脚本——shell函数详解