Apache Commons Exec 配置参数详解
详解 Apache Commons Exec 1.6.0 的运行时参数:工作目录、环境变量、ExecuteWatchdog 超时、PumpStreamHandler 流处理、退出值判定与异步结果处理,每个默认值与行为均附实测回显。
Apache Commons Exec 配置参数详解
目标:说清 Apache Commons Exec 1.6.0 在运行时可以配置的每一类参数——工作目录、环境变量、超时、流处理、退出值与异步结果处理,给出各自的默认值、配置方式与实测行为。所有默认值与回显已在 Ubuntu 22.04 + JDK 17 上实测通过。
Apache Commons Exec 的配置不像服务端中间件那样集中在配置文件里,而是通过 DefaultExecutor、PumpStreamHandler、ExecuteWatchdog 三个对象的 setter / builder 参数在代码中设定。本文先给出参数总览,再逐类实测其默认值与改动效果。
💡 技术拓展:在 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 完全兼容,本文用 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-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
本文所有示例都按 javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/ConfigDemo.java 编译、按 java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.ConfigDemo 运行,-d classes 把 class 输出到 classes 目录。
⚠️ 注意:本文示例源码统一放在
/opt/exec-lab/src/net/quzuo/demo/下,包名为net.quzuo.demo;源码含中文字符串,编译必须带-encoding UTF-8,运行必须带-Dfile.encoding=UTF-8,否则中文会显示为乱码。
三、参数查看与修改方式
Commons Exec 的参数没有集中配置文件,只有三种修改入口:
- Builder 链式设置(推荐):
DefaultExecutor.builder().setWorkingDirectory(...).setExecuteStreamHandler(...).get(); - Setter 直接设置:拿到 executor 后调用
executor.setWatchdog(...)、executor.setExitValues(...); - execute 重载传参:
executor.execute(cmd, env)与executor.execute(cmd, handler)在执行时临时传入环境变量或异步处理器。
三种方式对应的对象与作用域如下表,后续章节逐个展开。
| 参数对象 | 构造方式 | 作用 |
|---|---|---|
DefaultExecutor | DefaultExecutor.builder().get() | 执行器主体,承载工作目录、流处理、退出值判定 |
PumpStreamHandler | new PumpStreamHandler(out, err) | stdout / stderr / stdin 的流向 |
ExecuteWatchdog | ExecuteWatchdog.builder().setTimeout(...).get() | 子进程超时与强制终止 |
ExecuteResultHandler | new DefaultExecuteResultHandler() | 异步执行结果回调 |
⚠️ 注意:Commons Exec 1.6.0 中无参构造
new DefaultExecutor()已标记@Deprecated,实测编译会出现[deprecation] DefaultExecutor() in DefaultExecutor has been deprecated警告,请统一改用DefaultExecutor.builder().get()。
四、参数总览
下表汇总可配置参数、默认值与实测依据。默认值中「继承」表示沿用当前 JVM 进程的取值。
| 参数 | 配置入口 | 默认值(实测 / 源码) | 实测依据 |
|---|---|---|---|
| 工作目录 workingDirectory | DefaultExecutor.builder().setWorkingDirectory(File/Path) | Paths.get("."),即执行器进程当前目录 | 本章第五节 pwd 回显 |
| 环境变量 environment | execute(cmd, Map) 或 setEnvironment | Paths.get(".") 之外为继承父进程环境 | 本章第六节 printenv 回显 |
| 超时 timeout | ExecuteWatchdog.builder().setTimeout(Duration) | ExecuteWatchdog.builder() 默认无限;Watchdog.builder() 默认 30 秒 | 本章第七节 DefaultsDemo 回显 |
| 无限超时常量 | ExecuteWatchdog.INFINITE_TIMEOUT | -1(长整型) | 本章第七节回显 |
| stdout/stderr 流向 | PumpStreamHandler(OutputStream, OutputStream) | 无参构造输出到 System.out / System.err | 本章第八节回显 |
| stdin 流向 | PumpStreamHandler(OutputStream, OutputStream, InputStream) | 未设置则不向子进程写入数据 | 本章第八节回显 |
| 泵线程停止超时 stopTimeout | PumpStreamHandler.setStopTimeout(Duration) | Duration.ZERO | 源码 stopTimeout = Duration.ZERO |
| 成功退出值 | DefaultExecutor.setExitValue(int) / setExitValues(int[]) | new int[0],仅 0 视为成功 | 本章第九节 isFailure 回显 |
| 异步结果 | DefaultExecuteResultHandler | 收到退出时才置结果标志 | 本章第十节 hasResult 回显 |
表格解读:三类参数中,超时的默认值最需要留意——ExecuteWatchdog.builder() 与 Watchdog.builder() 的默认超时并不一致(无限 vs 30 秒),若混用会让「没有超时」的意图落空;退出值默认只认 0,业务命令返回 1、3 等都需要显式声明。
五、工作目录配置
本章演示 setWorkingDirectory。它决定子进程的当前工作目录(即命令里相对路径的基准)。
编写 ConfigDemo.java 的工作目录片段:
DefaultExecutor executor = DefaultExecutor.builder()
.setWorkingDirectory(new File("/opt/exec-lab"))
.setExecuteStreamHandler(new PumpStreamHandler(out, System.err))
.get();
int exit = executor.execute(CommandLine.parse("pwd"));
System.out.println("退出码: " + exit);
System.out.println("子进程工作目录: " + out.toString(StandardCharsets.UTF_8).trim());
编译运行该程序:
cd /opt/exec-lab
javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/ConfigDemo.java
java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.ConfigDemo
实测回显(输出节选,仅第 1 段):
===== 1) 工作目录 setWorkingDirectory =====
退出码: 0
子进程工作目录: /opt/exec-lab
相对路径 ls -1 结果:
classes
lib
src
pwd 返回 /opt/exec-lab,证明工作目录已生效;随后 ls -1(相对路径)列出的是 /opt/exec-lab 下的内容而非 JVM 启动目录。
实测还发现一个必踩的坑:把工作目录设成一个不存在的路径时,execute 直接抛 IOException,而非静默失败。实测在目录不存在时运行同段代码:
Exception in thread "main" java.io.IOException: /opt/commons doesn't exist.
at org.apache.commons.exec.DefaultExecutor.checkWorkingDirectory(DefaultExecutor.java:244)
at org.apache.commons.exec.DefaultExecutor.execute(DefaultExecutor.java:305)
因此上线前应确保工作目录真实存在,或先 Files.createDirectories 建好再传入。
六、环境变量配置
本章演示环境变量。子进程默认继承父进程环境;若要新增或覆盖变量,需要用 execute(cmd, env) 传入一个完整的环境 Map——注意这个 Map 是全量替换,不是增量合并,所以要先取父环境再追加自定义项。
环境变量片段:
Map<String, String> env = new HashMap<>(EnvironmentUtils.getProcEnvironment());
env.put("EXEC_LAB_TOKEN", "quzuo-2026");
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, System.err))
.get();
executor.execute(CommandLine.parse("printenv EXEC_LAB_TOKEN"), env);
System.out.println("子进程读取 EXEC_LAB_TOKEN: " + out.toString(StandardCharsets.UTF_8).trim());
EnvironmentUtils.getProcEnvironment() 返回当前进程的全部环境变量,复制后放入自定义项,就能在保留 PATH 等基础变量的同时新增变量。
实测回显(输出节选,仅第 2 段):
===== 2) 环境变量 setEnvironment =====
子进程读取 EXEC_LAB_TOKEN: quzuo-2026
子进程通过 printenv EXEC_LAB_TOKEN 读到了注入值 quzuo-2026,证明环境变量已生效。若把 Map 直接 new HashMap<>() 而不复制父环境,子进程会失去 PATH,导致 sh、ls 等命令找不到而返回退出码 127。
七、超时控制
本章讲解超时,它是 Commons Exec 相对 JDK 原生方案最有价值的参数。超时由看门狗 ExecuteWatchdog 承担:构造时给出超时 Duration,超过阈值即 destroyProcess() 杀掉子进程。
💡 技术拓展:看门狗有两个入口,默认值不同,务必区分。
| 类 | 推荐构造方式 | builder 默认超时 | 说明 |
|---|---|---|---|
Watchdog | Watchdog.builder().setTimeout(...).get() | 30 秒 | 通用看门狗,getTimeout() 可查当前阈值 |
ExecuteWatchdog | ExecuteWatchdog.builder().setTimeout(...).get() | 无限(INFINITE_TIMEOUT_DURATION) | 持有进程句柄、可 destroyProcess() |
本文实际使用 ExecuteWatchdog,并显式调用 setTimeout,避免依赖默认值。
实测两个默认值:Watchdog.builder().get().getTimeout() 与 ExecuteWatchdog 的无限超时(DefaultsDemo 输出节选):
Watchdog.builder().get().getTimeout() = PT30S
ExecuteWatchdog.INFINITE_TIMEOUT = -1
ExecuteWatchdog.INFINITE_TIMEOUT_DURATION = PT-0.001S
===== A) ExecuteWatchdog.builder() 不设超时(默认无限)=====
结果: 进程自然结束(未超时)
实际耗时(ms): 40012
Watchdog.builder() 默认 30 秒(PT30S);而 ExecuteWatchdog.builder() 不设超时即无限——实测 sleep 40 跑到 40012ms 自然结束,未被打断。两者差异一目了然。
设置 5 秒超时后再跑同一个 sleep 40(DefaultsDemo 输出节选):
===== B) 显式 5 秒超时 =====
结果: Process exited with an error: 143 (Exit value: 143)
killedProcess(): true
实际耗时(ms): 5010
子进程在 5010ms 处被杀,退出值为 143(128 + 15,即 SIGTERM),killedProcess() 返回 true。这说明超时到达后看门狗发的是终止信号,退出码并非业务码。
在综合示例 ConfigDemo 中,2 秒超时跑 sleep 5 的回显一致:
===== 3) 超时 ExecuteWatchdog =====
捕获 ExecuteException: Process exited with an error: 143 (Exit value: 143)
watchdog.killedProcess(): true
耗时约(ms): 2006
需要「永不超时、由代码显式杀进程」时,可传 ExecuteWatchdog.INFINITE_TIMEOUT(-1)或 INFINITE_TIMEOUT_DURATION,再在需要时调用 watchdog.destroyProcess()。
八、流处理配置
本章讲解流处理。PumpStreamHandler 负责把子进程的 stdout、stderr 泵到指定 OutputStream,并把外部输入写入子进程 stdin,避免管道缓冲区被写满导致死锁。
PumpStreamHandler 的三种构造形态及其流向:
| 构造方式 | stdout 去向 | stderr 去向 | stdin |
|---|---|---|---|
new PumpStreamHandler() | System.out | System.err | 不写入 |
new PumpStreamHandler(out) | out | out(同一流,须线程安全) | 不写入 |
new PumpStreamHandler(out, err) | out | err | 不写入 |
new PumpStreamHandler(out, err, in) | out | err | 从 in 读取并写入子进程 |
本文使用 new PumpStreamHandler(out, err) 分别捕获两路输出;若要「stdout 与 stderr 合并到同一条流」,用单参数的 PumpStreamHandler(out),但该流必须线程安全,因为输出与错误由两个线程并发写入。
流处理片段:
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
CommandLine script = new CommandLine("sh");
script.addArgument("-c", false);
script.addArgument("echo 这是STDOUT; echo 这是STDERR 1>&2", false);
executor.execute(script);
实测回显(输出节选,仅第 4 段):
===== 4) 流处理 PumpStreamHandler =====
--- stdout 捕获 ---
这是STDOUT
--- stderr 捕获 ---
这是STDERR
--- 标准输入流(PumpStreamHandler 三参数)---
输入数据一
输入数据二
三路流都验证到位:这是STDOUT 与 这是STDERR 被分流到两个独立缓冲,第三参数演示把预设文本通过 cat 回显,证明 stdin 方向也可控。补充一个默认值:PumpStreamHandler 的 stopTimeout 默认是 Duration.ZERO,可用 setStopTimeout(Duration) 在停止泵线程时放宽等待。
九、退出值判定
本章讲解退出值。DefaultExecutor.execute 返回子进程退出码,但默认只有 0 视为成功,其余退出码都会抛 ExecuteException。
退出值片段:
int exit = executor.execute(CommandLine.parse("echo done"));
System.out.println("echo done 退出码: " + exit);
CommandLine exit3 = new CommandLine("sh");
exit3.addArgument("-c", false);
exit3.addArgument("exit 3", false);
try {
executor.execute(exit3);
} catch (ExecuteException e) {
System.out.println("非零退出抛出: " + e.getMessage());
System.out.println("e.getExitValue(): " + e.getExitValue());
}
DefaultExecutor executor2 = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(out, System.err))
.get();
executor2.setExitValues(new int[]{0, 3});
int exit3v = executor2.execute(exit3);
System.out.println("setExitValues({0,3}) 后 exit 3 视为成功,返回: " + exit3v);
System.out.println("isFailure(3): " + executor2.isFailure(3));
实测回显(输出节选,仅第 5 段与第 7 段):
===== 5) 退出值 =====
echo done 退出码: 0
非零退出抛出: Process exited with an error: 3 (Exit value: 3)
e.getExitValue(): 3
setExitValues({0,3}) 后 exit 3 视为成功,返回: 3
isFailure(3): false
===== 7) 常量与默认值 =====
ExecuteWatchdog.INFINITE_TIMEOUT = -1
默认 isFailure(0) = false
默认 isFailure(1) = true
逐项解读:默认 isFailure(0)=false、isFailure(1)=true,说明成功集合初始只有 0;exit 3 触发 ExecuteException,其 getExitValue() 正是 3;调用 setExitValues(new int[]{0,3}) 后,exit 3 不再报错而是正常返回 3,且 isFailure(3) 变为 false。若某些脚本用 1 表示「有差异但可接受」,用 setExitValues 显式声明即可避免误判为失败。
十、异步执行配置
本章讲解异步执行。同步 execute 会阻塞调用线程直到子进程结束;异步则通过 ExecuteResultHandler 回调,主线程可立即继续。DefaultExecuteResultHandler 内置了一个 waitFor(),既能异步又能按需同步等待。
异步片段:
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
CommandLine asyncCmd = new CommandLine("sh");
asyncCmd.addArgument("-c", false);
asyncCmd.addArgument("sleep 2; echo 异步输出", false);
long start = System.currentTimeMillis();
executor.execute(asyncCmd, handler);
System.out.println("发起异步后主线程立即继续,耗时(ms): " + (System.currentTimeMillis() - start));
System.out.println("hasResult() 立即检查: " + handler.hasResult());
handler.waitFor();
System.out.println("waitFor() 返回,异步退出码: " + handler.getExitValue());
实测回显(输出节选,仅第 6 段):
===== 6) 异步执行 ExecuteResultHandler =====
发起异步后主线程立即继续,耗时(ms): 2
hasResult() 立即检查: false
waitFor() 返回,异步退出码: 0
异步 stdout: 异步输出
execute 返回仅耗时 2ms,主线程未被阻塞;此时立即 hasResult() 为 false(任务尚未结束);调用 waitFor() 阻塞到子进程结束后返回,getExitValue() 为 0,输出也被捕获。异步 + 超时的组合是长任务管控的常用形态:把 ExecuteWatchdog 与 ExecuteResultHandler 一起挂到 executor 上,超时由看门狗负责,结果由回调通知。
十一、生产配置模板
本章把上述参数组合成一份可直接套用的执行器模板,适用于「执行外部命令 + 限时 + 捕获输出 + 异常可控」的常见场景。
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 org.apache.commons.exec.environment.EnvironmentUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class SafeExec {
public static void main(String[] args) throws Exception {
Map<String, String> env = new HashMap<>(EnvironmentUtils.getProcEnvironment());
env.put("EXEC_LAB_TOKEN", "quzuo-2026");
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
ExecuteWatchdog watchdog = ExecuteWatchdog.builder()
.setTimeout(Duration.ofSeconds(10))
.get();
DefaultExecutor executor = DefaultExecutor.builder()
.setWorkingDirectory(new File("/opt/exec-lab"))
.setExecuteStreamHandler(new PumpStreamHandler(out, err))
.get();
executor.setWatchdog(watchdog);
executor.setExitValues(new int[]{0, 3});
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("echo 工作目录=$(pwd); echo 令牌=$EXEC_LAB_TOKEN", false);
int exit = executor.execute(cmd, env);
System.out.println("退出码: " + exit);
System.out.print(out.toString(StandardCharsets.UTF_8));
}
}
模板说明:setWorkingDirectory 固定 /opt/exec-lab;setEnvironment 以父环境为底追加令牌;ExecuteWatchdog 显式 10 秒超时;PumpStreamHandler 分流捕获;setExitValues 放行 0 与 3。该模板适配内存与磁盘都充足的一般服务器,10 秒超时可按命令预期耗时调整。
编译运行并核对输出:
cd /opt/exec-lab
javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/SafeExec.java
java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.SafeExec
实测回显:
退出码: 0
工作目录=/opt/exec-lab
令牌=quzuo-2026
三条配置(工作目录、环境变量、退出值)在同一段执行里同时生效,可作为线上执行器的起点。
十二、常见问题
Q1:ExecuteWatchdog.builder().get() 不设超时,为什么进程一直跑不停?
实测 ExecuteWatchdog.builder() 的默认超时是「无限」(INFINITE_TIMEOUT_DURATION,值 PT-0.001S),sleep 40 会跑到 40012ms 自然结束(见第七节 DefaultsDemo 回显)。要限时必须显式 setTimeout(Duration);注意 Watchdog.builder() 默认是 30 秒(PT30S),两者不要混淆。
Q2:超时被杀后退出码为什么是 143?
143 = 128 + 15,代表进程被 SIGTERM 终止(见第七节回显 Exit value: 143),不是业务退出码。业务侧应配合 killedProcess() 判断「是否因超时被杀」,而不是解析 143 这个数字。
Q3:setWorkingDirectory 传了不存在的目录会怎样?
实测直接抛 java.io.IOException: <目录> doesn't exist.(见第五节回显),程序中断。上线前请确保目录存在,或用 Files.createDirectories 先建好。
Q4:注入环境变量后,子进程里 sh/ls 反而找不到了?
execute(cmd, env) 的 Map 是全量替换。若只 new HashMap<>() 放入自定义项,会丢失 PATH,子进程返回退出码 127(命令未找到)。正确做法是以 EnvironmentUtils.getProcEnvironment() 为底再追加自定义项(见第六节)。
Q5:为什么源码里 new DefaultExecutor() 编译有 deprecation 警告?
1.6.0 起无参构造已弃用,改写为 DefaultExecutor.builder().get()(见第三节提示)。
十三、参考链接
- Apache Commons Exec 官方主页:https://commons.apache.org/proper/commons-exec/
- 官方 javadoc(1.6.0 API 文档):https://commons.apache.org/proper/commons-exec/apidocs/
- 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
- PumpStreamHandler 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/PumpStreamHandler.html
- Maven 中央仓库 1.6.0 目录:https://repo1.maven.org/maven2/org/apache/commons/commons-exec/1.6.0/
- 1.6.0 发行包下载:https://downloads.apache.org/commons/exec/binaries/commons-exec-1.6.0-bin.tar.gz