Java中调用外部脚本程序的方法总结
方案
Runtime
直接通过 Runtime 类的 exec() 方法执行命令来创建进程,执行外部程序。
- 字符串形式:如果使用单个字符串作为命令,需要确保命令格式正确,特别是空格和引号的使用。
String command = "java -version";
Process process = Runtime.getRuntime().exec(command);
- 字符串数组形式:使用字符串数组可以更清晰地分隔命令和参数,避免因空格等字符导致的问题。
String[] command = {"java", "-version"};
Process process = Runtime.getRuntime().exec(command);
int exitCode = process.waitFor();//等待进程结束
System.out.println("Process exited with code: " + exitCode);
ProcessBuilder
通过创建 ProcessBuilder 类的实例来使用。可以先设置各种参数,如运行命令、工作目录、环境变量等,然后再启动进程。
- 无参构造
使用无参构造函数创建 ProcessBuilder 对象后,可以通过 command() 方法设置要执行的命令和参数。command()支持可变长参数和List。此外command()方法也可以获取ProcessBuilder的命令和参数。
ProcessBuilder pb = new ProcessBuilder();
pb.command("ls", "-l", "/home/user")
- 可变长参数 String… command。命令和参数必须分开传递,不能将它们合并成一个字符串(如 “ls -l /home/user”)。ProcessBuilder 会将每个字符串作为独立的参数传递给操作系统。
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/home/user");
// 3.List或Array
List<String> command = new ArrayList<>();
command.add("java");
command.add("-jar");
command.add("myapp.jar");
ProcessBuilder processBuilder = new ProcessBuilder(command);
// 设置工作目录
pb.directory(new File("C:\\Users\\username\\Desktop"));
// 设置环境变量
pb.environment().put("PATH", "C:\\Program Files\\Java\\jdk-11\\bin");
// 启动
Process p = pb.start();
int exitCode = process.waitFor();//等待进程结束
System.out.println("Process exited with code: " + exitCode);
方案对比
参数设置灵活性
- ProcessBuilder参数设置更为灵活和详细。可以方便地设置工作目录、环境变量等。可以单独设置某个环境变量的值,或者完全替换整个环境变量集合。还可以设置运行命令的各个参数,以列表的形式传入,这样可以避免因空格等特殊字符导致的命令解析错误。
- Runtime.getRuntime().exec()参数设置相对简单,主要通过字符串形式传入命令及参数。对于一些复杂的命令,尤其是包含多个参数和特殊字符的命令,可能需要额外的处理来确保正确执行。
进程控制
- ProcessBuilder:提供了更丰富的进程控制方法。可以通过
redirectErrorStream() 方法将错误输出流重定向到标准输出流,方便统一处理输出信息。还可以通过inheritIO() 方法让子进程继承 Java 程序的标准输入、输出和错误流。 - Runtime.getRuntime().exec():在进程控制方面相对简单,主要通过
Process 类的getInputStream()、getOutputStream()、getErrorStream() 方法来获取进程的输入、输出和错误流,对进程的控制和交互相对较为基础。
异常处理
- ProcessBuilder:在创建ProcessBuilder 实例和设置参数时不会抛出异常,只有在调用start() 方法启动进程时,才会抛出IOException。这使得异常处理相对较为集中,便于在启动进程时统一处理可能出现的错误。
- Runtime.getRuntime().exec():在执行 exec() 方法时会抛出
IOException。如果命令字符串格式错误、找不到可执行文件等情况都会导致异常。需要在调用 exec() 方法时进行异常捕获和处理。
适用场景
- ProcessBuilder:适用于需要精细控制外部进程的情况,如需要设置特定的工作目录、环境变量,或者对进程的输入输出流进行复杂处理的场景。例如在开发一些需要调用外部命令行工具,并且对工具的运行环境和输出结果有严格要求的应用程序时,ProcessBuilder 是更好的选择。
- Runtime.getRuntime().exec():适用于简单的外部命令执行场景,当只需要快速执行一个命令,并且对命令的参数、运行环境等没有太多特殊要求时,使用Runtime.getRuntime().exec() 更为简洁方便。可以在一些简单的脚本执行、快速调用系统命令等场景中。
输出流(标准/错误)处理
如果不处理输出流将会有如下风险
- 缓冲区溢出
当外部进程向标准输出或错误输出写入大量数据时,这些数据会被放入缓冲区。如果缓冲区满了而没有读取,进程可能会被阻塞,无法继续执行,直到缓冲区中有空间可以写入新的数据. - 资源占用
即使进程结束了,如果缓冲区中的数据没有被读取,这些数据仍然占用着内存资源。虽然在进程结束后,操作系统最终会回收这些资源,但在进程结束之前,这些资源是无法被释放的. - 错误信息丢失
如果不读取错误输出流,将无法获取外部进程执行过程中可能出现的错误信息。这会导致调试和问题排查变得困难,因为你无法了解外部进程为何失败或出现问题. - 影响程序稳定性
在某些情况下,缓冲区溢出可能导致Java程序的不稳定,甚至出现异常或崩溃。这是因为外部进程的输出可能会不断累积,最终导致Java程序的资源耗尽.
输出流处理方法
- 通用处理方法
获取Process对象后,直接读取InputStream,ErrorStream流处理
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
- ProcessBuilder的特殊处理方法
1.将错误流重定向到标准输出流
redirectErrorStream方法用于将进程的错误输出流(stderr)重定向到标准输出流(stdout)。这样,错误信息和正常输出信息都会通过同一个流输出,方便统一处理。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class RedirectErrorStreamExample {
public static void main(String[] args) {
// 创建 ProcessBuilder 对象并设置命令和参数
ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", "nonexistent.jar");
// 将错误流重定向到标准输出流
processBuilder.redirectErrorStream(true);
try {
// 启动进程
Process process = processBuilder.start();
// 读取标准输出流(同时包含错误信息)
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println("进程输出: " + line);
}
// 等待进程结束并获取退出码
int exitCode = process.waitFor();
System.out.println("进程退出码: " + exitCode);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
2.重定向到文件
可以将输出和错误流重定向到文件,以便后续查看:
ProcessBuilder processBuilder = new ProcessBuilder("command", "arg1", "arg2");
processBuilder.redirectOutput(new File("output.log")); // 重定向标准输出
processBuilder.redirectError(new File("error.log")); // 重定向标准错误
Process process = processBuilder.start();
3.丢弃输出
如果完全不关心输出内容,可以将输出和错误流重定向到 null 设备(在 Unix 系统中是 /dev/null,在 Windows 系统中是 NUL):
ProcessBuilder processBuilder = new ProcessBuilder("command", "arg1", "arg2");
processBuilder.redirectOutput(ProcessBuilder.Redirect.DISCARD); // 丢弃标准输出
processBuilder.redirectError(ProcessBuilder.Redirect.DISCARD); // 丢弃标准错误
Process process = processBuilder.start();
愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!