Apache Commons Exec 快速入门与集成示例

用 Apache Commons Exec 1.6.0 从零跑通四个快速入门示例:同步执行捕获输出、超时看门狗杀进程、异步执行回调、传参与环境变量/工作目录,并整合成一个完整可运行程序,全程附实测回显。

Apache Commons ExecApache Commons Exec2026/10/14

Apache Commons Exec 快速入门与集成示例

目标:用最短路径把 Apache Commons Exec 1.6.0 跑起来——从同步执行、超时看门狗、异步回调到传参与环境变量,四个示例逐个跑通,最后整合成一个完整可运行程序。所有命令与回显已在 Ubuntu 22.04 + JDK 17 上实测通过。

Apache Commons Exec 解决的是「在 Java 里可靠地执行外部命令」这件事:它会帮你处理输出流、超时和异步回调这些手工做起来容易出错的部分。本文按「由简到繁」的顺序给出四个可独立运行的示例,再合成一个完整程序,适合作为上手第一站。

💡 技术拓展:在 JVM 里执行外部命令,业界有多种方案,选型前先看清各自边界。

方案所属特点适用场景
Runtime.exec()JDK 内置最简单,无超时、无流处理,易造成管道缓冲区死锁一次性简单调用
ProcessBuilderJDK 内置支持重定向与合并输出流,但超时/异步仍需自行实现可控的中等复杂度调用
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()(见第五节)。

十、参考链接

  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. 官方快速入门示例(官方首页「Quick Start」):https://commons.apache.org/proper/commons-exec/
  4. DefaultExecutor 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/DefaultExecutor.html
  5. ExecuteWatchdog 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/ExecuteWatchdog.html
  6. PumpStreamHandler 类文档:https://commons.apache.org/proper/commons-exec/apidocs/org/apache/commons/exec/PumpStreamHandler.html
  7. Maven 中央仓库 1.6.0 目录:https://repo1.maven.org/maven2/org/apache/commons/commons-exec/1.6.0/
  8. 1.6.0 发行包下载:https://downloads.apache.org/commons/exec/binaries/commons-exec-1.6.0-bin.tar.gz