Apache Commons Exec 快速入门与集成示例
用 Apache Commons Exec 1.6.0 从零跑通四个快速入门示例:同步执行捕获输出、超时看门狗杀进程、异步执行回调、传参与环境变量/工作目录,并整合成一个完整可运行程序,全程附实测回显。
Apache Commons Exec 快速入门与集成示例
目标:用最短路径把 Apache Commons Exec 1.6.0 跑起来——从同步执行、超时看门狗、异步回调到传参与环境变量,四个示例逐个跑通,最后整合成一个完整可运行程序。所有命令与回显已在 Ubuntu 22.04 + JDK 17 上实测通过。
Apache Commons Exec 解决的是「在 Java 里可靠地执行外部命令」这件事:它会帮你处理输出流、超时和异步回调这些手工做起来容易出错的部分。本文按「由简到繁」的顺序给出四个可独立运行的示例,再合成一个完整程序,适合作为上手第一站。
💡 技术拓展:在 JVM 里执行外部命令,业界有多种方案,选型前先看清各自边界。
| 方案 | 所属 | 特点 | 适用场景 |
|---|---|---|---|
Runtime.exec() | JDK 内置 | 最简单,无超时、无流处理,易造成管道缓冲区死锁 | 一次性简单调用 |
ProcessBuilder | JDK 内置 | 支持重定向与合并输出流,但超时/异步仍需自行实现 | 可控的中等复杂度调用 |
| JNA / JNI | 第三方 / 原生 | 直接调用系统 API,能力最强但需平台库,部署重 | 需操作系统级控制的场景 |
| Apache Commons Exec | 独立库 | 统一封装执行、流泵、超时看门狗、异步结果处理,零传递依赖 | 需要超时控制与输出捕获的工程 |
本文使用 Apache Commons Exec 1.6.0:不引入任何第三方运行期依赖,一个 jar 就能编译运行全部示例。
一、环境信息
本章确认实测环境。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
二、前置条件
本章准备实验目录、依赖 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
准备被命令访问的目标目录:
mkdir -p /opt/commons/reports /opt/commons/data
printf "id,name\n1,demo\n" > /opt/commons/data/users.csv
ls -1 /opt/commons
实测回显:
data
reports
本文示例统一放在 /opt/exec-lab/src/net/quzuo/demo/,包名 net.quzuo.demo,编译运行约定如下:
| 环节 | 命令 |
|---|---|
| 编译 | javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/<类>.java |
| 运行 | java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.<类> |
⚠️ 注意:源码含中文字符串,编译必须带
-encoding UTF-8,运行必须带-Dfile.encoding=UTF-8,否则中文会显示为乱码。
三、示例一:同步执行并捕获输出
本章是最基础的用法——同步执行一条命令,把它的输出收进缓冲再打印。同步执行会阻塞到子进程结束,execute 返回退出码。
示例代码 QuickStartDemo 步骤1:
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(stdout, stderr))
.get();
int exit = executor.execute(CommandLine.parse("grep root /etc/passwd"));
System.out.println("退出码: " + exit);
System.out.println("isFailure(exit): " + executor.isFailure(exit));
System.out.print(stdout.toString(StandardCharsets.UTF_8));
把该段编译运行的完整流程:
cd /opt/exec-lab
javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/QuickStartDemo.java
java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.QuickStartDemo
实测回显(QuickStartDemo 输出节选,仅步骤1):
===== 步骤1:同步执行并捕获输出 =====
退出码: 0
isFailure(exit): false
root:x:0:0:root:/root:/bin/bash
三行输出确认:命令退出码 0、isFailure(0) 为 false、grep 命中的 /etc/passwd 行被完整捕获。要点是 PumpStreamHandler 把 stdout/stderr 分别导向两个 ByteArrayOutputStream,避免子进程写满管道缓冲区。
四、示例二:带超时的看门狗杀进程
本章给执行加一道「保险」——用 ExecuteWatchdog 限制最长运行时间,超时即强制终止子进程。
示例代码步骤2:
ExecuteWatchdog watchdog = ExecuteWatchdog.builder()
.setTimeout(Duration.ofSeconds(3))
.get();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(System.out, System.err))
.get();
executor.setWatchdog(watchdog);
long start = System.currentTimeMillis();
try {
executor.execute(CommandLine.parse("sleep 30"));
} catch (ExecuteException e) {
System.out.println("进程被看门狗终止: " + e.getMessage());
}
System.out.println("killedProcess(): " + watchdog.killedProcess());
System.out.println("实际耗时(ms): " + (System.currentTimeMillis() - start));
实测回显(QuickStartDemo 输出节选,仅步骤2):
===== 步骤2:看门狗超时杀进程 =====
进程被看门狗终止: Process exited with an error: 143 (Exit value: 143)
killedProcess(): true
实际耗时(ms): 3007
sleep 30 在约 3007ms 处被终止(不是 30 秒),抛出 ExecuteException,killedProcess() 返回 true。超时退出值 143(128 + 15,SIGTERM)不是业务码,判断「是否因超时被杀」应看 killedProcess()。
五、示例三:异步执行与结果回调
本章演示不阻塞——用 execute(cmd, resultHandler) 提交命令后主线程立即返回,子进程结束时通过回调通知结果。
示例代码步骤3:
CountDownLatch latch = new CountDownLatch(1);
final int[] exitHolder = new int[1];
ExecuteResultHandler handler = new ExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
exitHolder[0] = exitValue;
System.out.println("回调 onProcessComplete, exitValue=" + exitValue);
latch.countDown();
}
@Override
public void onProcessFailed(ExecuteException e) {
System.out.println("回调 onProcessFailed: " + e.getMessage());
latch.countDown();
}
};
CommandLine asyncCmd = new CommandLine("sh");
asyncCmd.addArgument("-c", false);
asyncCmd.addArgument("sleep 2; echo 异步任务完成", false);
executor.execute(asyncCmd, handler);
System.out.println("提交后主线程继续执行,未阻塞");
latch.await();
System.out.println("最终退出码: " + exitHolder[0]);
实测回显(QuickStartDemo 输出节选,仅步骤3):
===== 步骤3:异步执行 + 自定义回调 =====
提交后主线程继续执行,未阻塞
异步任务完成
回调 onProcessComplete, exitValue=0
最终退出码: 0
execute 立即返回、主线程打印「未阻塞」后继续;子进程 2 秒后打印「异步任务完成」,随后触发 onProcessComplete 回调、退出码 0。回调里用 CountDownLatch 通知主线程的做法,适合需要「提交即返回、按需汇总结果」的批量任务。
六、示例四:传参、环境变量与工作目录
本章把三个常用要素合在一处演示——位置参数、注入环境变量、指定工作目录。
示例代码步骤4:
Map<String, String> env = new HashMap<>(EnvironmentUtils.getProcEnvironment());
env.put("EXEC_LAB_TOKEN", "quzuo-2026");
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setWorkingDirectory(new File("/opt/exec-lab"))
.setExecuteStreamHandler(new PumpStreamHandler(stdout, System.err))
.get();
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("echo 工作目录=$(pwd); echo 环境变量=$EXEC_LAB_TOKEN; echo 位置参数=$0", false);
cmd.addArgument("自定义参数值", false);
int exit = executor.execute(cmd, env);
System.out.println("退出码: " + exit);
System.out.print(stdout.toString(StandardCharsets.UTF_8));
实测回显(QuickStartDemo 输出节选,仅步骤4):
===== 步骤4:传参、环境变量与工作目录 =====
退出码: 0
工作目录=/opt/exec-lab
环境变量=quzuo-2026
位置参数=自定义参数值
三行分别验证:$(pwd) 返回 /opt/exec-lab(setWorkingDirectory 生效)、$EXEC_LAB_TOKEN 读到注入值 quzuo-2026(环境变量生效)、$0 读到追加的位置参数(addArgument 生效)。
两点提醒:一是 execute(cmd, env) 的环境 Map 是全量替换,必须先复制 EnvironmentUtils.getProcEnvironment() 再放自定义项,否则子进程会丢 PATH 而返回退出码 127;二是 setWorkingDirectory 指向不存在的目录会直接抛 java.io.IOException: <目录> doesn't exist.,上线前请确保目录存在。
七、整合:一个完整可运行程序
本章把四个示例合成一个 QuickStartDemo,依次执行四步,可一次性复现全部回显。
package net.quzuo.demo;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteResultHandler;
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;
import java.util.concurrent.CountDownLatch;
public class QuickStartDemo {
public static void main(String[] args) throws Exception {
step1SyncCapture();
step2WatchdogTimeout();
step3AsyncCallback();
step4EnvAndWorkingDir();
}
private static void step1SyncCapture() throws Exception {
System.out.println("===== 步骤1:同步执行并捕获输出 =====");
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(stdout, stderr))
.get();
int exit = executor.execute(CommandLine.parse("grep root /etc/passwd"));
System.out.println("退出码: " + exit);
System.out.println("isFailure(exit): " + executor.isFailure(exit));
System.out.print(stdout.toString(StandardCharsets.UTF_8));
}
private static void step2WatchdogTimeout() throws Exception {
System.out.println("===== 步骤2:看门狗超时杀进程 =====");
ExecuteWatchdog watchdog = ExecuteWatchdog.builder()
.setTimeout(Duration.ofSeconds(3))
.get();
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(System.out, System.err))
.get();
executor.setWatchdog(watchdog);
long start = System.currentTimeMillis();
try {
executor.execute(CommandLine.parse("sleep 30"));
} catch (ExecuteException e) {
System.out.println("进程被看门狗终止: " + e.getMessage());
}
System.out.println("killedProcess(): " + watchdog.killedProcess());
System.out.println("实际耗时(ms): " + (System.currentTimeMillis() - start));
}
private static void step3AsyncCallback() throws Exception {
System.out.println("===== 步骤3:异步执行 + 自定义回调 =====");
CountDownLatch latch = new CountDownLatch(1);
final int[] exitHolder = new int[1];
ExecuteResultHandler handler = new ExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
exitHolder[0] = exitValue;
System.out.println("回调 onProcessComplete, exitValue=" + exitValue);
latch.countDown();
}
@Override
public void onProcessFailed(ExecuteException e) {
System.out.println("回调 onProcessFailed: " + e.getMessage());
latch.countDown();
}
};
DefaultExecutor executor = DefaultExecutor.builder()
.setExecuteStreamHandler(new PumpStreamHandler(System.out, System.err))
.get();
CommandLine asyncCmd = new CommandLine("sh");
asyncCmd.addArgument("-c", false);
asyncCmd.addArgument("sleep 2; echo 异步任务完成", false);
executor.execute(asyncCmd, handler);
System.out.println("提交后主线程继续执行,未阻塞");
latch.await();
System.out.println("最终退出码: " + exitHolder[0]);
}
private static void step4EnvAndWorkingDir() throws Exception {
System.out.println("===== 步骤4:传参、环境变量与工作目录 =====");
Map<String, String> env = new HashMap<>(EnvironmentUtils.getProcEnvironment());
env.put("EXEC_LAB_TOKEN", "quzuo-2026");
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
DefaultExecutor executor = DefaultExecutor.builder()
.setWorkingDirectory(new File("/opt/exec-lab"))
.setExecuteStreamHandler(new PumpStreamHandler(stdout, System.err))
.get();
CommandLine cmd = new CommandLine("sh");
cmd.addArgument("-c", false);
cmd.addArgument("echo 工作目录=$(pwd); echo 环境变量=$EXEC_LAB_TOKEN; echo 位置参数=$0", false);
cmd.addArgument("自定义参数值", false);
int exit = executor.execute(cmd, env);
System.out.println("退出码: " + exit);
System.out.print(stdout.toString(StandardCharsets.UTF_8));
}
}
编译运行:
cd /opt/exec-lab
javac -encoding UTF-8 -cp 'lib/*' -d classes src/net/quzuo/demo/QuickStartDemo.java
java -Dfile.encoding=UTF-8 -cp 'classes:lib/*' net.quzuo.demo.QuickStartDemo
实测回显(完整输出):
===== 步骤1:同步执行并捕获输出 =====
退出码: 0
isFailure(exit): false
root:x:0:0:root:/root:/bin/bash
===== 步骤2:看门狗超时杀进程 =====
进程被看门狗终止: Process exited with an error: 143 (Exit value: 143)
killedProcess(): true
实际耗时(ms): 3007
===== 步骤3:异步执行 + 自定义回调 =====
提交后主线程继续执行,未阻塞
异步任务完成
回调 onProcessComplete, exitValue=0
最终退出码: 0
===== 步骤4:传参、环境变量与工作目录 =====
退出码: 0
工作目录=/opt/exec-lab
环境变量=quzuo-2026
位置参数=自定义参数值
整段输出与前面四节的分步回显一致,四类能力在一个程序里全部跑通。
八、集成要点与生产建议
本章给出把上述示例用到实际工程时值得固化的几条经验。
| 要点 | 建议做法 | 原因 |
|---|---|---|
| 构建方式 | 用 DefaultExecutor.builder().get() 而非 new DefaultExecutor() | 1.6.0 起无参构造已弃用 |
| 成功码 | 用 setExitValues(new int[]{0, n}) 声明业务可接受码 | 默认仅 0 视为成功,其余抛异常 |
| 超时 | 显式 ExecuteWatchdog.builder().setTimeout(...) | ExecuteWatchdog.builder() 默认无限,易漏设 |
| 环境变量 | 以 EnvironmentUtils.getProcEnvironment() 为底追加 | Map 全量替换,直接新建会丢 PATH |
| 工作目录 | 传入前确保目录存在,必要时 Files.createDirectories | 指向不存在的目录会抛 IOException |
| 输出处理 | 大输出用流式 PumpStreamHandler(out) 或 LogOutputStream | 收进单个缓冲可能占内存 |
| 字符集 | 统一 UTF-8(编译 -encoding UTF-8、运行 -Dfile.encoding=UTF-8) | 否则中文乱码 |
补充:CommandLine 支持 ${GREETING} 这类变量替换,可在提交前用 setSubstitutionMap 展开固定参数;对必须跨平台的场景,用 OS.isFamilyUnix() 分流命令写法,OS.isName 精确比对时注意区分大小写(实测 isName("linux") 为 true、isName("Linux") 为 false)。
九、常见问题
Q1:执行报 ExecuteException: Process exited with an error: 1,命令明明正常?
默认只有退出码 0 视为成功。业务命令返回 1、3 等非零码时,用 setExitValues(new int[]{0, 1}) 声明即可(见第八节)。
Q2:超时到底设多少?为什么 sleep 30 3 秒就结束了?
超时按「命令的预期最长耗时」设;本文示例刻意设 3 秒在 sleep 30 上,用来演示看门狗生效(见第四节/步骤2 回显 实际耗时(ms): 3007)。
Q3:注入了环境变量,子进程里 sh/ls 却找不到了?
execute(cmd, env) 的环境 Map 是全量替换。以 EnvironmentUtils.getProcEnvironment() 为底再放自定义项(见第六节),否则丢 PATH 会返回退出码 127。
Q4:setWorkingDirectory 传了不存在的目录会怎样?
实测直接抛 java.io.IOException: <目录> doesn't exist.(见第六节),程序中断,请先建好目录。
Q5:异步提交后拿不到结果怎么办?
自定义 ExecuteResultHandler 时需自行等待(示例用 CountDownLatch);若用 DefaultExecuteResultHandler,waitFor() 不重抛异常,判成败要看 getException()(见第五节)。
十、参考链接
- Apache Commons Exec 官方主页:https://commons.apache.org/proper/commons-exec/
- 官方 javadoc(1.6.0 API 文档):https://commons.apache.org/proper/commons-exec/apidocs/
- 官方快速入门示例(官方首页「Quick Start」):https://commons.apache.org/proper/commons-exec/
- 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