• 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);  

    好了,今天介绍到这里。

  • 相关阅读:
    ES vm报错
    ln -s /usr/local/jdk1.8.0_201/bin/java /bin/java
    docker压缩导入导出
    微软各种资源整理(迅雷下载),感谢站长。
    python打开文件的访问方式
    docker换源
    selinux
    ElasticsearchException: java.io.IOException: failed to read [id:0, file:/data/elasticsearch/nodes/0/_state/global-0.st]
    带了纸和笔,要记哪些东西?
    redis命令行批量删除匹配到的key
  • 原文地址:https://www.cnblogs.com/hyl8218/p/8302552.html
Copyright © 2020-2023  润新知