Apache Commons Exec 常用 CLI 命令与工具详解
逐类讲解 Apache Commons Exec 1.6.0 核心类与调用形态:CommandLine 解析与变量替换、DefaultExecutor 执行、流处理、超时看门狗、异步回调、OS 判定,附自写执行工具实测回显。
Apache Commons Exec 常用 CLI 命令与工具详解
目标:把 Apache Commons Exec 1.6.0 的核心类当成一套「命令执行工具」逐类讲清其调用形态与真实运行结果——从
CommandLine构造、DefaultExecutor执行,到流处理、超时看门狗、异步回调与自定义流处理器。所有命令与回显已在 Ubuntu 22.04 + JDK 17 上实测通过。
Apache Commons Exec 本身是 Java 库,没有独立的命令行程序;所谓「CLI 与工具」,指的是「用 javac/java -cp 编译运行、并按核心类拼出执行外部命令的工具代码」这一整套用法。本文先把核心类职责排开,再逐类给出可编译运行的调用形态与实测输出,最后给出一个自写的通用执行工具。
💡 技术拓展:在 JVM 里执行外部命令,业界有多种方案,选型前先看清各自边界。
| 方案 | 所属 | 特点 | 适用场景 |
|---|---|---|---|
Runtime.exec() | JDK 内置 | 最简单,无超时、无流处理,易造成管道缓冲区死锁 | 一次性简单调用 |
ProcessBuilder | JDK 内置 | 支持重定向与合并输出流,但超时/异步仍需自行实现 | 可控的中等复杂度调用 |
| JNA / JNI | 第三方 / 原生 | 直接调用系统 API,能力最强但需平台库,部署重 | 需操作系统级控制的场景 |
| Apache Commons Exec | 独立库 | 统一封装执行、流泵、超时看门狗、异步结果处理,零传递依赖 | 需要超时控制与输出捕获的工程 |
本文所有示例都基于 Apache Commons Exec 1.6.0,它把「执行 + 超时 + 输出捕获 + 异步回调」封装成一组可组合对象;下文逐个说明这些对象。
一、环境信息
本章确认实测环境与库版本。Commons Exec 1.6.0 的字节码目标是 Java 8,用 JDK 17 编译运行完全兼容。
确认系统与 Java 版本:
cat /etc/os-release | grep -E "^VERSION="
java -version 2>&1 | head -1
实测回显:
VERSION="22.04.5 LTS (Jammy Jellyfish)"
openjdk version "17.0.20.1" 2026-08-18
二、工具概览
本章先给出核心类清单与职责,明确「哪个类干哪件事」,后续章节逐类展开调用形态。Commons Exec 的常用类共 8 个,按职责可分为「命令描述」「执行主体」「流处理」「超时与结果」四组。
| 类 | 分组 | 核心职责 | 常用入口 |
|---|---|---|---|
CommandLine | 命令描述 | 描述可执行文件与参数,支持引号与 ${GREETING} 这类变量替换 | new CommandLine(...)、CommandLine.parse(...)、addArgument(...) |
DefaultExecutor | 执行主体 | 执行命令,承载工作目录、退出值判定、看门狗 | DefaultExecutor.builder().get()、execute(...) |
PumpStreamHandler | 流处理 | 把子进程 stdout/stderr 泵到输出流,把输入写入 stdin | new PumpStreamHandler(out, err) |
ExecuteStreamHandler | 流处理 | 流处理抽象接口,可自定义实现 | setProcessOutputStream(...) 等 |
StreamPumper | 流处理 | 单条流的搬运线程体(Runnable) | new StreamPumper(is, os, true) |
LogOutputStream | 流处理 | 把流入数据按行回调给你 | new LogOutputStream() |
ExecuteWatchdog | 超时 | 监控进程执行时长,超时后强制终止 | ExecuteWatchdog.builder().setTimeout(...).get() |
ExecuteResultHandler | 结果 | 异步执行结果回调接口 | onProcessComplete(...)、onProcessFailed(...) |
OS | 环境判定 | 判定操作系统家族、名称、架构 | OS.isFamilyUnix()、OS.isName("linux") |
表格解读:一次完整的「执行外部命令」通常只需要前三行——CommandLine 描述、DefaultExecutor 执行、PumpStreamHandler 收流;需要限时再加 ExecuteWatchdog,需要不阻塞再加 ExecuteResultHandler。OS 与 ExecuteStreamHandler 属于按需使用的辅助类。
三、前置条件
本章准备一个独立实验目录并引入 commons-exec-1.6.0.jar,同时备好演示用的目标目录。Commons Exec 零传递依赖,只需一个主 jar 即可编译运行下文的全部示例。
创建目录并下载主 jar:
mkdir -p /opt/exec-lab/lib /opt/exec-lab/src/net/quzuo/demo
curl -fsSL -o /opt/exec-lab/lib/commons-exec-1.6.0.jar https://repo1.maven.org/maven2/org/apache/commons/commons-exec/1.6.0/commons-exec-1.6.0.jar
ls -l /opt/exec-lab/lib/
实测回显:
total 68
-rw-r--r-- 1 root root 69316 Oct 13 16:54 commons-exec-1.6.0.jar
再准备一个被命令访问的目标目录,供 ls、grep 等示例使用:
mkdir -p /opt/commons/reports /opt/commons/data
printf "sample report\n" > /opt/commons/reports/q3.txt
printf "id,name\n1,demo\n" > /opt/commons/data/users.csv
printf "readme content\n" > /opt/commons/README.md
ls -1 /opt/commons
实测回显:
README.md
data
reports
⚠️ 注意:本文示例源码统一放在
/opt/exec-lab/src/net/quzuo/demo/,包名为net.quzuo.demo;源码含中文字符串,编译必须带-encoding UTF-8,运行必须带-Dfile.encoding=UTF-8,否则中文会显示为乱码。带包名的类按javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/<类>.java编译、按java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.<类>运行。
四、CommandLine 详解
本章讲解 CommandLine——描述「执行什么命令、带什么参数」。它有三种构造方式:直接 new CommandLine("可执行文件")、用 CommandLine.parse(整条命令) 解析、或对已有对象 addArgument(...) 追加。
4.1 构造与解析
CommandLine.parse 把整条命令按空白切成「可执行文件 + 参数数组」;toStrings() 返回可执行文件与参数拼接后的完整数组,getArguments() 只返回参数部分。
CommandLine fromParse = CommandLine.parse("ls -l /opt/commons");
System.out.println("parse 后可执行文件: " + fromParse.getExecutable());
System.out.println("parse 后参数数组: " + Arrays.toString(fromParse.getArguments()));
System.out.println("toStrings(): " + Arrays.toString(fromParse.toStrings()));
实测回显(CliDemo 输出节选,仅第 1 段):
===== 1) CommandLine 构造与解析 =====
parse 后可执行文件: ls
parse 后参数数组: [-l, /opt/commons]
toStrings(): [ls, -l, /opt/commons]
getExecutable() 返回 ls,getArguments() 返回 [-l, /opt/commons],与输入一致。
4.2 addArgument 与引号处理
addArgument(String, boolean) 的第二个布尔参数决定「是否对参数做引号处理」:传 true 时会按 shell 规则给含空格的参数加引号,传 false 时原样传递。下例用 sh -c 跑一段含空格的脚本,并把位置参数 $1/$2 逐字加入:
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("echo 参数一: $1; echo 参数二: $2", false);
cmd.addArgument("值一", true);
cmd.addArgument("值二", true);
System.out.println("toStrings(): " + Arrays.toString(cmd.toStrings()));
实测回显(CliDemo 输出节选,仅第 2 段):
===== 2) addArgument 追加与是否处理引号 =====
toStrings(): [sh, -c, echo 参数一: $1; echo 参数二: $2, 值一, 值二]
含空格的脚本被整体作为一个参数传递(false 未加引号),这也是 sh -c 能收到完整脚本的原因。
💡 技术拓展:把命令交给外部执行时,「怎样把参数拼成一条命令」有好几种方式,处理引号与空格的可靠度差别很大。
| 方式 | 示例 | 空格/引号处理 | 可靠性 |
|---|---|---|---|
| 字符串硬拼 | Runtime.exec("sh -c 'echo x'") | 全靠自己拼,易错 | 存在注入与解析风险 |
CommandLine.parse(String) | CommandLine.parse("ls -l /opt") | 按空白切分,不解析引号 | 简单命令可用,含引号不可靠 |
CommandLine + addArgument(arg, true) | 逐参数追加并加引号 | 库自动按规则加引号 | 推荐,参数可控 |
本文统一使用 CommandLine + addArgument:实测把带引号的复合命令整条交给 CommandLine.parse("sh -c 'echo hi'") 时,参数被错误切分,进程返回退出码 127;改用「可执行文件与脚本分别 addArgument」后即恢复正常。
4.3 变量替换 setSubstitutionMap
CommandLine 支持把参数里的变量占位(形如 ${GREETING})在取参数时按 setSubstitutionMap 传入的 Map 展开:
Map<String, Object> map = new HashMap<>();
map.put("GREETING", "hello");
map.put("TARGET", "commons-exec");
CommandLine sub = new CommandLine("echo");
sub.addArgument("${GREETING} ${TARGET}", false);
sub.setSubstitutionMap(map);
System.out.println("替换前模板: [echo, ${GREETING} ${TARGET}]");
System.out.println("替换后 toStrings(): " + Arrays.toString(sub.toStrings()));
实测回显(CliDemo 输出节选,仅第 3 段):
===== 3) 变量替换 setSubstitutionMap =====
替换前模板: [echo, ${GREETING} ${TARGET}]
替换后 toStrings(): [echo, hello commons-exec]
模板里的 ${GREETING} ${TARGET} 在 toStrings() 读取时被展开为 hello commons-exec,说明变量替换发生在「读取参数」这一步,原参数串保持不变。
五、DefaultExecutor 详解
本章讲解 DefaultExecutor——执行命令的主体。1.6.0 中无参构造 new DefaultExecutor() 已标记 @Deprecated,推荐用 DefaultExecutor.builder().get() 构建,再用 execute(...) 的三种重载执行。
三种 execute 重载:
| 重载 | 形态 | 用途 |
|---|---|---|
execute(CommandLine) | 同步执行 | 阻塞到结束,返回退出码或抛 ExecuteException |
execute(CommandLine, Map<String,String>) | 同步 + 环境变量 | 以给定环境执行(全量替换) |
execute(CommandLine, ExecuteResultHandler) | 异步执行 | 立即返回,结果由回调通知 |
同步执行的最小形态:
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
int exit = executor.execute(CommandLine.parse("echo hello commons exec"));
System.out.println("退出码: " + exit);
System.out.println("isFailure(exit): " + executor.isFailure(exit));
实测回显(HelloExec 输出节选,仅 echo 段):
命令: echo hello commons exec
退出码: 0
hello commons exec
commons-exec 版本: 1.6.0
验证通过
execute 返回退出码 0,isFailure(0) 为 false;末行还从 jar 清单打印出库版本 1.6.0。需要判断「退出码算不算失败」时,用 executor.isFailure(exit);要放宽成功集合可用 setExitValues(new int[]{0, 3})。
六、PumpStreamHandler 与流处理
本章讲解 PumpStreamHandler——负责「收流」,避免子进程写满管道缓冲区而卡死。它有三个构造形态,掌握后即可决定 stdout、stderr 各自的去向。
| 构造方式 | stdout 去向 | stderr 去向 | stdin |
|---|---|---|---|
new PumpStreamHandler() | System.out | System.err | 不写入 |
new PumpStreamHandler(out, err) | out | err | 不写入 |
new PumpStreamHandler(out, err, in) | out | err | 从 in 读入写入子进程 |
把两路输出分别收进内存缓冲:
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
executor.execute(CommandLine.parse("ls -1 /opt/commons"));
System.out.print(out.toString(StandardCharsets.UTF_8));
实测回显(ExecTool 输出节选):
[exit] 0
[stdout]
README.md
data
reports
[stderr]
stdout 被完整捕获、stderr 为空,证明分流构造正确。若用单参数 new PumpStreamHandler(out),则 stdout 与 stderr 合流到同一条流,此时该流必须线程安全,因为两路输出由两个线程并发写入。
七、ExecuteWatchdog 详解
本章讲解 ExecuteWatchdog——超时看门狗。构造时给一个超时 Duration,子进程超过阈值即被 destroyProcess() 强制终止(发 SIGTERM),killedProcess() 可事后查询是否被杀。
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
executor.setWatchdog(ExecuteWatchdog.builder()
.setTimeout(Duration.ofSeconds(2))
.get());
executor.execute(CommandLine.parse("sleep 5"));
实测回显(ExecTool -t 2 sleep 5):
[fail] Process exited with an error: 143 (Exit value: 143)
[stdout]
[stderr]
子进程在 2 秒处被终止,抛出 ExecuteException,退出值 143(128 + 15,即 SIGTERM),并非业务退出码。要「永不超时、由代码显式杀进程」时,可传 ExecuteWatchdog.INFINITE_TIMEOUT(-1)再在需要时调用 destroyProcess()。注意 ExecuteWatchdog.builder() 不设超时的默认值是无限,Watchdog.builder() 的默认值才是 30 秒,两者不要混淆。
八、ExecuteResultHandler 异步结果
本章讲解 ExecuteResultHandler——异步结果回调接口,让主线程提交命令后立即返回。它有两个方法:onProcessComplete(int) 在成功时回调,onProcessFailed(ExecuteException) 在失败时回调。
自己实现一个回调,用 CountDownLatch 等待结果:
CountDownLatch latch = new CountDownLatch(1);
ExecuteResultHandler handler = new ExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
System.out.println("onProcessComplete 回调, exitValue=" + exitValue);
latch.countDown();
}
@Override
public void onProcessFailed(ExecuteException e) {
System.out.println("onProcessFailed 回调: " + e.getMessage());
latch.countDown();
}
};
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("sleep 1; echo 自定义回调任务结束", false);
executor.execute(cmd, handler);
System.out.println("execute 已返回,主线程继续");
latch.await();
实测回显(CliAsyncDemo 输出节选,仅 A 段):
===== A) 自定义 ExecuteResultHandler =====
execute 已返回,主线程继续
自定义回调任务结束
onProcessComplete 回调, exitValue=0
execute 立即返回、主线程打印「已返回」后继续,子进程结束后才触发 onProcessComplete。库自带 DefaultExecuteResultHandler 已实现该接口并提供 waitFor(),适合「异步提交、按需同步取值」:
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
executor.execute(failCmd, handler);
handler.waitFor();
System.out.println("失败场景 hasResult(): " + handler.hasResult());
System.out.println("失败场景 getExitValue(): " + handler.getExitValue());
System.out.println("失败场景 getException(): " + handler.getException());
实测回显(CliAsyncDemo 输出节选,仅 B 段):
===== B) DefaultExecuteResultHandler 内置实现 =====
内置处理器成功任务结束
成功场景 hasResult(): true
成功场景 getExitValue(): 0
成功场景 getException(): null
内置处理器失败任务结束
失败场景 hasResult(): true
失败场景 getExitValue(): 7
失败场景 getException(): org.apache.commons.exec.ExecuteException: Process exited with an error: 7 (Exit value: 7)
一个必须留意的实测结论:DefaultExecuteResultHandler.waitFor() 不重抛异常。失败任务 exit 7 时,waitFor() 正常返回,失败信息留在 getException() 里(成功场景该值为 null),退出码从 getExitValue() 读取。判断成败请查 getException(),而不要指望 waitFor() 抛出。
九、ExecuteStreamHandler 接口与自定义实现
本章讲解 ExecuteStreamHandler——PumpStreamHandler 是它的默认实现,必要时可自行实现,把三条流接到任意地方。接口要覆盖四个方法:setProcessOutputStream(InputStream)、setProcessErrorStream(InputStream)、setProcessInputStream(OutputStream)、start() / stop()。
下面用 StreamPumper 起两个线程分别收 stdout 与 stderr,stop() 时 join 线程:
static class CollectingStreamHandler implements ExecuteStreamHandler {
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final ByteArrayOutputStream err = new ByteArrayOutputStream();
private Thread outThread;
private Thread errThread;
@Override
public void setProcessOutputStream(InputStream is) {
outThread = new Thread(new StreamPumper(is, out, true));
outThread.start();
}
@Override
public void setProcessErrorStream(InputStream is) {
errThread = new Thread(new StreamPumper(is, err, true));
errThread.start();
}
@Override
public void setProcessInputStream(OutputStream os) throws IOException {
os.close();
}
@Override
public void start() {
}
@Override
public void stop() throws IOException {
join(outThread);
join(errThread);
}
private void join(Thread thread) {
if (thread == null) {
return;
}
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
String stdout() {
return out.toString(StandardCharsets.UTF_8);
}
String stderr() {
return err.toString(StandardCharsets.UTF_8);
}
}
setProcessInputStream 里直接 os.close(),表示子进程不需要标准输入、关闭管道即触发 EOF。用它执行一条同时输出 stdout 与 stderr 的脚本:
CollectingStreamHandler handler = new CollectingStreamHandler();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(handler)
.get();
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("echo 自定义处理器捕获的 STDOUT; echo 自定义处理器捕获的 STDERR 1>&2", false);
int exit = executor.execute(cmd);
System.out.println("退出码: " + exit);
System.out.println("自定义 handler stdout: " + handler.stdout().trim());
System.out.println("自定义 handler stderr: " + handler.stderr().trim());
实测回显(StreamHandlerDemo):
退出码: 0
自定义 handler stdout: 自定义处理器捕获的 STDOUT
自定义 handler stderr: 自定义处理器捕获的 STDERR
两条流分别落进自定义的缓冲,说明自定义 ExecuteStreamHandler 生效。若不自定义,直接用 PumpStreamHandler 更省事;LogOutputStream 则适合「边执行边按行打印」的场景。
十、OS 家族判定
本章讲解 OS——判定当前操作系统,常用于「按平台选择命令写法」。它提供三类方法:家族判定 isFamilyUnix()/isFamilyWindows() 等、名称判定 isName(...)、架构判定 isArch(...)。
System.out.println("isFamilyUnix(): " + OS.isFamilyUnix());
System.out.println("isFamilyWindows(): " + OS.isFamilyWindows());
System.out.println("isName(\"linux\"): " + OS.isName("linux"));
System.out.println("isName(\"Linux\"): " + OS.isName("Linux"));
System.out.println("isArch(\"amd64\"): " + OS.isArch("amd64"));
实测回显(CliDemo 输出节选,仅第 4 段):
===== 4) OS 家族判定 =====
isFamilyUnix(): true
isFamilyWindows(): false
isFamilyWinNT(): false
isFamilyMac(): false
isFamilyOS2(): false
isFamilyOpenVms(): false
isName("linux"): true
isName("Linux"): false
isArch("amd64"): true
逐项解读:isFamilyUnix() 为 true;isName("linux") 为 true 而 isName("Linux") 为 false——名称判定区分大小写,必须传小写;isArch("amd64") 为 true。
💡 技术拓展:判断操作系统有几种思路,精确度与可移植性不同。
| 方式 | 代码 | 特点 |
|---|---|---|
| 系统属性 | System.getProperty("os.name") | 返回 Linux 等原始串,大小写/厂商不定,需自己匹配 |
| 家族判定 | OS.isFamilyUnix() | 按族归类,跨发行版稳定 |
| 名称判定 | OS.isName("linux") | 精确比对,区分大小写 |
| 架构判定 | OS.isArch("amd64") | 区分 amd64/x86 等架构 |
本文示例用 OS.isFamilyUnix() 做跨平台分流(稳定、不挑大小写),需要精确到具体系统名时才用 OS.isName,并注意传小写。
十一、自写执行工具 ExecTool 实战
本章把前面的类组合成一个通用小工具 ExecTool——java -cp 直接运行,接收「可执行文件 + 参数」,可选超时,跑完打印退出码与 stdout/stderr。它是「用核心类拼工具」的完整示例。
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class ExecTool {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("用法: java ExecTool [-t 超时秒数] <可执行文件> [参数列表]");
return;
}
long timeoutSeconds = 0;
int start = 0;
if ("-t".equals(args[0])) {
timeoutSeconds = Long.parseLong(args[1]);
start = 2;
}
CommandLine cmd = new CommandLine(args[start]);
for (int i = start + 1; i < args.length; i++) {
cmd.addArgument(args[i], false);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
if (timeoutSeconds > 0) {
executor.setWatchdog(ExecuteWatchdog.builder()
.setTimeout(Duration.ofSeconds(timeoutSeconds)).get());
}
try {
int exit = executor.execute(cmd);
System.out.println("[exit] " + exit);
} catch (ExecuteException e) {
System.out.println("[fail] " + e.getMessage());
}
System.out.println("[stdout]");
System.out.print(out.toString(StandardCharsets.UTF_8));
System.out.println("[stderr]");
System.out.print(err.toString(StandardCharsets.UTF_8));
}
}
该工具无包名,编译与运行都用 java -cp 直指定类路径:
cd /opt/exec-lab
javac -encoding UTF-8 -cp 'lib/*' -d outtool ExecTool.java
编译输出 class 到 outtool,无回显。先看不带参数的用法回显:
java -Dfile.encoding=UTF-8 -cp 'outtool:lib/*' ExecTool
实测回显:
用法: java ExecTool [-t 超时秒数] <可执行文件> [参数列表]
执行目录列举命令:
java -Dfile.encoding=UTF-8 -cp 'outtool:lib/*' ExecTool ls -1 /opt/commons
实测回显:
[exit] 0
[stdout]
README.md
data
reports
[stderr]
执行带中文参数的回显命令:
java -Dfile.encoding=UTF-8 -cp 'outtool:lib/*' ExecTool echo 参数一 参数二
实测回显:
[exit] 0
[stdout]
参数一 参数二
[stderr]
加上 -t 2 的 2 秒超时执行 sleep 5:
java -Dfile.encoding=UTF-8 -cp 'outtool:lib/*' ExecTool -t 2 sleep 5
实测回显:
[fail] Process exited with an error: 143 (Exit value: 143)
[stdout]
[stderr]
四组回显覆盖了工具的用法提示、正常执行、中文传参与超时失败三个分支,说明 CommandLine、DefaultExecutor、PumpStreamHandler、ExecuteWatchdog 四类协作正确。
十二、常见调用形态与注意事项
本章汇总几类高频调用形态与对应注意点,便于照用。
| 场景 | 调用形态 | 注意点 |
|---|---|---|
| 同步取输出 | new PumpStreamHandler(out, err) + execute(cmd) | 默认仅退出码 0 视为成功,其他会抛 ExecuteException |
| 限时执行 | executor.setWatchdog(ExecuteWatchdog.builder().setTimeout(...).get()) | ExecuteWatchdog.builder() 默认无限;Watchdog.builder() 默认 30 秒 |
| 不阻塞 | execute(cmd, resultHandler) | 自定义接口需自行等待;DefaultExecuteResultHandler.waitFor() 不重抛异常 |
| 注入环境变量 | execute(cmd, env) | Map 全量替换,先复制 EnvironmentUtils.getProcEnvironment() 再追加 |
| 放宽成功码 | executor.setExitValues(new int[]{0, 3}) | 声明后 isFailure(3) 变 false |
| 跨平台分流 | OS.isFamilyUnix() / OS.isName("linux") | isName 区分大小写,传小写 |
两条最容易踩的经验:一是退出码默认只认 0,业务命令返回 1、3、7 都要显式声明,否则会被当异常;二是异步结果别指望 waitFor() 抛异常,判成败要看 getException()。
十三、常见问题
Q1:CommandLine.parse("sh -c 'echo x'") 为什么执行报错?
parse 按空白切分且不解析引号,带引号的复合命令会被错误拆开,实测返回退出码 127。改用「可执行文件与脚本分别 addArgument」即可(见第四节 4.2)。
Q2:new DefaultExecutor() 编译出现 deprecation 警告怎么办?
1.6.0 起无参构造已弃用,改用 DefaultExecutor.builder().get()(见第五节)。
Q3:DefaultExecuteResultHandler.waitFor() 为什么没抛异常?
实测该方法不重抛异常,失败信息保留在 getException();exit 7 时 getExitValue() 返回 7、getException() 为 ExecuteException(见第八节 B 段回显)。
Q4:OS.isName("Linux") 为什么返回 false?
isName 区分大小写,实测 isName("linux") 为 true、isName("Linux") 为 false(见第十节),请传小写。
Q5:超时被杀后退出码为什么是 143?
143 = 128 + 15,代表被 SIGTERM 终止(见第七节回显 Exit value: 143),不是业务退出码;配合 killedProcess() 判断是否因超时被杀。
十四、参考链接
- Apache Commons Exec 官方主页:https://commons.apache.org/proper/commons-exec/
- 官方 javadoc(1.6.0 API 文档):https://commons.apache.org/proper/commons-exec/apidocs/
- CommandLine 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/CommandLine.html
- DefaultExecutor 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/DefaultExecutor.html
- ExecuteWatchdog 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/ExecuteWatchdog.html
- ExecuteResultHandler 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/ExecuteResultHandler.html
- ExecuteStreamHandler 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/ExecuteStreamHandler.html
- OS 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/OS.html
- Maven 中央仓库 1.6.0 目录:https://repo1.maven.org/maven2/org/apache/commons/commons-exec/1.6.0/