本文记录的是如何在Java类中调用外部可执行脚本,例如shell脚本、Python脚本、ruby脚本等。文中阐述了Runtime.exec和ProcessBuilder.start两种调用脚本的方式,官方推荐使用后者的方式,下面对其分别讲述。

以Runtime.exec的方式调用脚本

代码是最直观说明问题的方式,先贴上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public void executeScript() {
//获取可执行脚本路径|scriptPath|及其参数文本路径|logPath|
String scriptPath = Thread.currentThread().getContextClassLoader()
.getResource("log_analysis.sh").getPath();
String logPath = Thread.currentThread().getContextClassLoader()
.getResource("log.txt").getPath();

Runtime runtime = Runtime.getRuntime();
String[] cmdarray = { "sh", scriptPath, logPath };
BufferedReader br = null;
BufferedReader bre = null;
try {
Process process = runtime.exec(cmdarray);
//读取标准输出流
br = new BufferedReader(new InputStreamReader(
process.getInputStream()));
StringBuffer linesBuffer = new StringBuffer();
String line = null;
while (null != (line = br.readLine())) {
linesBuffer.append(line).append("\n");
}
System.out.println("get output: \n" + linesBuffer.toString());
//读取标准错误输出流
bre = new BufferedReader(new InputStreamReader(
process.getErrorStream()));
StringBuffer errorBuffer = new StringBuffer();
String errorline = null;
while (null != (errorline = bre.readLine())) {
errorBuffer.append(errorline).append("\n");
}
System.out.println("get error output: \n" + errorBuffer.toString());

//等到脚本执行完毕,获取返回码|exitValue|
int exitValue = process.waitFor();
if (0 == exitValue) {
System.out.println("execute script success.");
} else {
System.out.println("execute script failed: exitValue = "
+ exitValue);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("execute script failed:" + e);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("execute script failed:" + e);
} finally {
if (null != br) {
try {
br.close();
br = null;
} catch (IOException e) {
e.printStackTrace();
}
}

if (null != bre) {
try {
bre.close();
bre = null;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

在代码的开始(2-6行),获取脚本文件以及参数文件的路径(scriptPath/logPath),这两个文件都放置在项目的资源文件夹下。关于资源文件路径的获取可参考:Java获取文件的路径.

代码8-13行是调用外部应用的核心代码。java.lang.Runtime的静态方法getRuntime()会获取当前Java Runtime Environment,这是获取Runtime对象的唯一途径,通过该引用,可以通过调用Runtime类的exec()方法运行外部应用。

代码14-31行是通过进程的输入输出流获取该进程的输入输出,对于进程有大量的输入输出信息时,则必须使用上述代码读取进程的输入输出流,在JDK's Javadoc中有如下描述:

Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.

如果不读取该进程的输入输出流,则可能会导致该进程的挂起。关于使用Runtime.exec()的一些陷阱可参考When Runtime.exec() won't,该文有详细的阐述;更健壮的方案是启动两个线程,一个线程负责读标准输出流,另一个负责读标准错误流,该文有涉及,需要的童鞋可点击超链接查看。

代码33-40行的waitFor()方法使当前线程等待直至Process对象运行结束,如果子进程已结束则该方法立即返回,如果子进程未结束,则当前线程会阻塞直至子进程退出。该方法的返回(int exitValue)代表子进程的退出码,0指示正常终止退出。按照我的理解,该exitValue类似于在Linux shell终端环境下执行命令后的$?的值,如下:

1
2
3
4
5
6
7
8
$ ls 
ExecuteScriptDemo
$ echo $?
0
$ ls -l noExistFile
ls: noExistFile: No such file or directory
$ echo $?
1

最后测试上述方法,将log_analysis.sh文件中第4行执行python的调用代码注释和不注释两种情况,分别运行测试代码,可得到下面两种情况的输出:

  • 注释python调用的代码情况输出
1
2
3
4
5
6
7
8
9
10
get output: 
APUE
Linux
Spring
Java
Objective-c

get error output:

execute script success.
  • 不注释python调用的代码情况输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
get output: 
APUE
Linux
Spring
Java
Objective-c

get error output:
File "/Users/carya/workspace/github/ExecuteScriptDemo/target/classes/log.txt", line 1
2014.05.24 ExecuteScript.class APUE
^
SyntaxError: invalid syntax

execute script failed: exitValue = 1

以ProcessBuilder.start方式调用脚本

同上,首先贴出代码,主要注意代码第8-17行与上一种方法的不同之处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public void executeScript2() {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
String scriptPath = classLoader.getResource("log_analysis.sh")
.getPath();
String logPath = classLoader.getResource("log.txt").getPath();

List<String> commandsList = new ArrayList<String>();
commandsList.add("sh");
commandsList.add(scriptPath);
commandsList.add(logPath);
ProcessBuilder processBuilder = new ProcessBuilder(commandsList);
//merge standard output stream and standard error output stream
processBuilder.redirectErrorStream(true);
BufferedReader br = null;
try {
Process process = processBuilder.start();

try {
//读取标准输出流和标准错误输出流
br = new BufferedReader(new InputStreamReader(
process.getInputStream()));
StringBuffer linesBuffer = new StringBuffer();
String line = null;
while (null != (line = br.readLine())) {
linesBuffer.append(line).append("\n");
}
System.out.println("get output: \n" + linesBuffer.toString());

int exitValue = process.waitFor();
if (0 == exitValue) {
System.out.println("execute script success.");
} else {
System.out.println("execute script failed: exitValue = "
+ exitValue);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("execute script failed:" + e);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("execute script failed:" + e);
} finally {
if (null != br) {
try {
br.close();
br = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

关于ProcessBuilder的详细描述可以参考其文档.其余对其输出流的读取等与上述Runtime.exec()相同。

Runtime.exec()与ProcessBuilder的不同

两者的主要不同之处体现在ProcessBuilder能够以更灵活,简单的方式定义新创建进程的运行环境,以及启动新进程。From Runtime.exec() to ProcessBuilder一文对此进行了对比,英文的,建议直接查看该文,下面对其进行简要的概括。

JDK 5.0之前,只能使用以下Runtime.exec()的六种方式创建进程调用外部应用,在调用exec()方法之前,需要指定调用的命令及其参数,设置环境变量和工作目录。每一个版本的exec()方法都会返回一个java.lang.Process对象来管理新创建的进程,这使你可以获取新创建进程的输入输出流以及返回码.

1
2
3
4
5
6
7
8
public Process exec(String command) throws IOException
public Process exec(String[] cmdarray) throws IOException

public Process exec(String command, String[] envp) throws IOException
public Process exec(String[] cmdarray, String[] envp) throws IOException

public Process exec(String command, String[] envp, File dir) throws IOException
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException

若以以下方法调用:

1
2
3
File file = new File(other directory);
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(command, null, file);

将会改变命令运行的工作目录,上述第二个参数是环境的设置,传入的是null,表示子进程继承当前进程的环境设置。

而,ProcessBuilder类有两个构造函数, 一个是以List的方式传入命令及命令参数,另一个以可变字符串的方式传入命令及命令参数:

1
2
public ProcessBuilder(List command)
public ProcessBuilder(String... command)

ProcessBuilder使用start()方法执行命令,在调用start()方法之前,可以改变Process创建的方式。如果想让process工作在不同的目录,与Runtime.exec()传入工作目录参数不同,ProcessBuilder调用directory()方法:

1
public ProcessBuilder directory(File directory)

ProcessBuilder没有显示的setter方法来改变进程的环境变量,你可以使用environment()方法获取当前环境变量列表:

1
2
3
ProcessBuilder processBuilder = new ProcessBuilder(command);
Map env = processBuilder.environment();
// manipulate env

通过操作该Map可以添加、更新、移除环境变量值:

1
2
3
4
ProcessBuilder processBuilder = new ProcessBuilder(command, arg1, arg2);
Map env = processBuilder.environment();
env.put("var1", "value");
env.remove("var3");

在环境变量和工作目录都设置好后,调用start()方法,执行命令:

1
2
processBuilder.directory("Dir");
Process p = processBuilder.start();

你可以使用clear()方法清除掉当前的所有环境变量设置,显示设置自己所需的环境。

除此之外,与Runtime.exec()不同之处,ProcessBuilder可使用redirectInput方法重定向输入源,以及使用redirectOutputredirectError重定向标准输出和标准错误输出流,这样的情况下,Process.getInputStream()Process.getErrorStream()将会返回null input stream:

  • 输入流的read方法总是返回-1
  • 输入流的available方法总是返回0
  • 输入流的close方法不会做任何操作

ProcessBuilder的redirectErrorStream属性默认值是false,这种情况下,标准输出和标准错误输出会输出到两个不同的输出流,二者可通过Process.getInputStream()Process.getErrorStream()分别获取.如果属性值设置为true,那么:

  • 标准输出和标准错误输出流会合并输出到同一目的地
  • 标准输出和标准错误输出流的共同目的地可通过redirectOutput方法重定向
  • 使用redirectError方法对标准错误输出流的重定向都会被忽略
  • 使用Process.getErrorStream()返回的会是null input stream

本文所涉及的代码托管在了GitHub上ExecuteScriptDemo.