Apache Commons Exec 配置参数详解

详解 Apache Commons Exec 1.6.0 的运行时参数:工作目录、环境变量、ExecuteWatchdog 超时、PumpStreamHandler 流处理、退出值判定与异步结果处理,每个默认值与行为均附实测回显。

Apache Commons ExecApache Commons Exec2026/10/14

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 内置最简单,无超时、无流处理,易造成管道缓冲区死锁一次性简单调用
ProcessBuilderJDK 内置支持重定向与合并输出流,但超时/异步仍需自行实现可控的中等复杂度调用
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 的参数没有集中配置文件,只有三种修改入口:

  1. Builder 链式设置(推荐):DefaultExecutor.builder().setWorkingDirectory(...).setExecuteStreamHandler(...).get();
  2. Setter 直接设置:拿到 executor 后调用 executor.setWatchdog(...)、executor.setExitValues(...);
  3. execute 重载传参:executor.execute(cmd, env) 与 executor.execute(cmd, handler) 在执行时临时传入环境变量或异步处理器。

三种方式对应的对象与作用域如下表,后续章节逐个展开。

参数对象构造方式作用
DefaultExecutorDefaultExecutor.builder().get()执行器主体,承载工作目录、流处理、退出值判定
PumpStreamHandlernew PumpStreamHandler(out, err)stdout / stderr / stdin 的流向
ExecuteWatchdogExecuteWatchdog.builder().setTimeout(...).get()子进程超时与强制终止
ExecuteResultHandlernew DefaultExecuteResultHandler()异步执行结果回调

⚠️ 注意:Commons Exec 1.6.0 中无参构造 new DefaultExecutor() 已标记 @Deprecated,实测编译会出现 [deprecation] DefaultExecutor() in DefaultExecutor has been deprecated 警告,请统一改用 DefaultExecutor.builder().get()。

四、参数总览

下表汇总可配置参数、默认值与实测依据。默认值中「继承」表示沿用当前 JVM 进程的取值。

参数配置入口默认值(实测 / 源码)实测依据
工作目录 workingDirectoryDefaultExecutor.builder().setWorkingDirectory(File/Path)Paths.get("."),即执行器进程当前目录本章第五节 pwd 回显
环境变量 environmentexecute(cmd, Map) 或 setEnvironmentPaths.get(".") 之外为继承父进程环境本章第六节 printenv 回显
超时 timeoutExecuteWatchdog.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)未设置则不向子进程写入数据本章第八节回显
泵线程停止超时 stopTimeoutPumpStreamHandler.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 默认超时说明
WatchdogWatchdog.builder().setTimeout(...).get()30 秒通用看门狗,getTimeout() 可查当前阈值
ExecuteWatchdogExecuteWatchdog.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.outSystem.err不写入
new PumpStreamHandler(out)outout(同一流,须线程安全)不写入
new PumpStreamHandler(out, err)outerr不写入
new PumpStreamHandler(out, err, in)outerr从 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()(见第三节提示)。

十三、参考链接

  1. Apache Commons Exec 官方主页:https://commons.apache.org/proper/commons-exec/
  2. 官方 javadoc(1.6.0 API 文档):https://commons.apache.org/proper/commons-exec/apidocs/
  3. DefaultExecutor 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/DefaultExecutor.html
  4. ExecuteWatchdog 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/ExecuteWatchdog.html
  5. PumpStreamHandler 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/PumpStreamHandler.html
  6. Maven 中央仓库 1.6.0 目录:https://repo1.maven.org/maven2/org/apache/commons/commons-exec/1.6.0/
  7. 1.6.0 发行包下载:https://downloads.apache.org/commons/exec/binaries/commons-exec-1.6.0-bin.tar.gz