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

从零演示 Apache Commons Daemon 集成全流程:编写实现 Daemon 接口的示例服务,编译后用 jsvc 以 256 至 512MB 堆启动并验证,-stop 优雅停止,并对比 systemd 托管形态。

Apache Commons DaemonApache Commons Daemon2026/10/13

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

目标:用一条完整链路跑通 Apache Commons Daemon:编写实现 Daemon 接口的示例服务,编译后用 jsvc 启动并做进程与日志双重验证,优雅停止后再给出 systemd 托管的替代形态,全部命令已在 Ubuntu 22.04 / CentOS 8(JDK 17 + Maven)实测通过。

一、环境信息

本文示例在以下环境实测执行,两条操作系统的命令完全一致(仅 JDK 目录名不同,文中有标注)。

项目Ubuntu 22.04 实测值CentOS 8 实测值
操作系统Ubuntu 22.04.5 LTSCentOS Linux 8
JDKOpenJDK 17.0.20.1OpenJDK 17.0.1
Maven3.6.33.5.4
commons-daemon1.6.11.6.1
jsvc源码编译产物(197032 字节)源码编译产物(199328 字节)

约定:jsvc 安装在 /usr/local/bin/jsvc,工作目录 /opt/demo-daemon,降权用户 appuser。示例类名统一为 DemoDaemon。

💡 技术拓展:把 Java 程序做成守护进程有四种常见方案,选型对比如下。

方案pid 管理降权运行日志信号处理开机自启
jsvcpidfile 记录-user 参数原生支持-outfile/-errfile 重定向内建优雅停机协议需配合 systemd 或 init 脚本
prunsrvWindows 服务管理器托管服务账号/凭据配置stdout/stderr 重定向到文件Windows 服务控制协议注册为 Windows 服务即自启
systemd Unitcgroup 全程托管User= 指令journald 或重定向KillSignal 可配WantedBy= 即自启
nohup+disown需手工记录 pid需 su/runuser 自行处理需自行重定向默认无优雅停机无,需自写开机脚本

本文主线为 jsvc,第七章给出 systemd Unit 托管同一服务的替代形态与对比结论;Windows 平台对应工具为 prunsrv。

二、准备 jsvc 与支持库

2.1 下载并校验发行包

sudo mkdir -p /opt/commons-daemon && cd /opt/commons-daemon
curl -fsSLO https://downloads.apache.org/commons/daemon/binaries/commons-daemon-1.6.1-bin.tar.gz
curl -fsSLO https://downloads.apache.org/commons/daemon/binaries/commons-daemon-1.6.1-bin.tar.gz.sha512
curl -fsSLO https://downloads.apache.org/commons/daemon/source/commons-daemon-1.6.1-src.tar.gz
curl -fsSLO https://downloads.apache.org/commons/daemon/source/commons-daemon-1.6.1-src.tar.gz.sha512
sha512sum -c commons-daemon-1.6.1-bin.tar.gz.sha512
sha512sum -c commons-daemon-1.6.1-src.tar.gz.sha512

回显(实测):

commons-daemon-1.6.1-bin.tar.gz: OK
commons-daemon-1.6.1-src.tar.gz: OK

两个校验值实测分别为:bin 包 4757860f7700908c 开头,src 包 51910df48e5e8dd4 开头(sha512 全文可用 cat 查看对应 .sha512 文件)。

2.2 编译 jsvc

bin 包只含 Java 支持库 jar,Linux 原生启动器 jsvc 必须从 src 包编译。前置条件:JDK 17 与 gcc、make 已安装(Ubuntu 22.04 执行 sudo apt install -y openjdk-17-jdk gcc make,CentOS 8 执行 sudo dnf install -y java-17-openjdk-devel gcc make glibc-devel)。

cd /opt/commons-daemon
tar -xzf commons-daemon-1.6.1-src.tar.gz
cd commons-daemon-1.6.1-src/src/native/unix
sudo ./configure --with-java=/usr/lib/jvm/java-17-openjdk-amd64

回显(实测节选,保留首尾关键行):

checking JAVA_HOME... /usr/lib/jvm/java-17-openjdk-amd64
checking for JDK os include directory...  linux
gcc flags added
checking for sys/capability.h... no
configure: WARNING: cannot find headers for libcap
*** Writing output files ***
configure: creating ./config.status
config.status: creating native/Makefile
*** All done ***
Now you can issue "make"
sudo make

回显(实测,保留末尾链接行):

ar cr libservice.a arguments.o debug.o dso-dlfcn.o dso-dyld.o help.o home.o java.o location.o replace.o locks.o signals.o
ranlib libservice.a
gcc   jsvc-unix.o libservice.a -ldl -lpthread -o ../jsvc
make[1]: Leaving directory '/opt/commons-daemon/commons-daemon-1.6.1-src/src/native/unix/native'
sudo cp jsvc /usr/local/bin/jsvc
/usr/local/bin/jsvc --help | tail -2

回显(实测):

jsvc (Apache Commons Daemon) 1.6.1
Copyright (c) 1999-2026 Apache Software Foundation.

CentOS 8 上两处差异:configure 的 --with-java 参数写 /usr/lib/jvm/java-17-openjdk;编译产物 199328 字节。其余输出一致。

2.3 解压支持库并准备目录

cd /opt/commons-daemon && tar -xzf commons-daemon-1.6.1-bin.tar.gz
sudo mkdir -p /opt/demo-daemon
sudo cp commons-daemon-1.6.1/commons-daemon-1.6.1.jar /opt/demo-daemon/
sudo useradd -m -s /bin/bash appuser
ls -l /opt/demo-daemon/

回显(实测):

total 32
-rw------- 1 root root 27078 Oct 13 07:45 commons-daemon-1.6.1.jar

2.4 Maven 项目集成说明

若服务本身是 Maven 工程,可在 pom 中直接引用官方支持库(实测该坐标可从 Maven Central 解析,jar 与 bin 包内 jar 的 sha512 一致):

<dependency>
    <groupId>commons-daemon</groupId>
    <artifactId>commons-daemon</artifactId>
    <version>1.6.1</version>
</dependency>

注意 groupId 必须写 commons-daemon:实测 Maven Central 上 org.apache.commons:commons-daemon 只有 2012 年的 1.0.9,用它引 1.6.1 会解析失败。本文为保持示例自包含,直接使用 bin 包内的 jar 编译,两种方式产物等价。

三、编写示例服务

创建 /opt/demo-daemon/DemoDaemon.java。实现 Daemon 接口的四个生命周期方法,init 记录入参,start 拉起心跳线程,stop 通知线程退出并等待,destroy 收尾:

import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.apache.commons.daemon.DaemonController;

import java.util.Arrays;

/**
 * 最小化 Apache Commons Daemon 示例服务。
 * 实现 Daemon 接口的四个生命周期方法:init / start / stop / destroy。
 */
public class DemoDaemon implements Daemon {

    private volatile boolean running = true;
    private Thread worker;
    private DaemonContext context;

    @Override
    public void init(DaemonContext context) throws Exception {
        this.context = context;
        System.out.println("[DemoDaemon] init() called, args="
                + Arrays.toString(context.getArguments()));
    }

    @Override
    public void start() throws Exception {
        System.out.println("[DemoDaemon] start() called, worker thread starting");
        running = true;
        worker = new Thread(() -> {
            int count = 0;
            while (running) {
                try {
                    Thread.sleep(5000);
                    if (running) {
                        count++;
                        System.out.println("[DemoDaemon] heartbeat #" + count
                                + ", thread=" + Thread.currentThread().getName());
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            System.out.println("[DemoDaemon] worker thread exited");
        }, "demo-worker");
        worker.start();
    }

    @Override
    public void stop() throws Exception {
        System.out.println("[DemoDaemon] stop() called, notifying worker to exit");
        running = false;
        if (worker != null) {
            worker.join(3000);
        }
    }

    @Override
    public void destroy() {
        System.out.println("[DemoDaemon] destroy() called, resources released");
    }
}

生命周期方法对应关系(语义详解见配置参数详解篇第十章):

方法jsvc 调用时机本例行为
initJVM 创建后、服务生效前保存 DaemonContext,打印启动参数
startinit 成功后启动 demo-worker 心跳线程
stop收到 -stop 或 SIGTERM置位 running 并等待线程退出
destroystop 完成后、进程退出前打印资源释放日志

四、编译与预检查

cd /opt/demo-daemon
sudo javac -encoding UTF-8 -cp commons-daemon-1.6.1.jar DemoDaemon.java
ls -l DemoDaemon.class

回显(实测):

-rw-r--r-- 1 root root 2841 Oct 13 07:49 DemoDaemon.class

-encoding UTF-8 不可省略:系统 locale 为 C 时 javac 默认按 US-ASCII 读取源码,含中文注释的源文件会报 error: unmappable character 而编译失败(实测踩坑)。启动前用 -check 做加载预检查:

sudo /usr/local/bin/jsvc -check -java-home /usr/lib/jvm/java-17-openjdk-amd64 -outfile '&1' -errfile '&2' -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon DemoDaemon

回显(实测):

Service "DemoDaemon" checked successfully

CentOS 8 上仅需把 -java-home 换成 /usr/lib/jvm/java-17-openjdk,实测同样输出 checked successfully。

五、jsvc 启动与验证

5.1 启动命令

root 启动、-user appuser 降权运行、堆固定 256 至 512MB、日志与 pidfile 显式落盘:

sudo /usr/local/bin/jsvc -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon -user appuser -Xms256m -Xmx512m -outfile /opt/demo-daemon/demo-stdout.log -errfile /opt/demo-daemon/demo-stderr.log -pidfile /opt/demo-daemon/demo-daemon.pid DemoDaemon
echo RC=$?

回显(实测):

RC=0

参数逐项说明:

参数作用
-java-home /usr/lib/jvm/java-17-openjdk-amd64锁定 JDK 17,避免多 JDK 环境落到系统默认的低版本 JVM
-cp jar:目录支持库 jar 与类文件目录两个条目
-user appuserroot 完成启动后降权到 appuser 运行 JVM
-Xms256m -Xmx512m初始堆 256MB,最大堆 512MB
-outfile / -errfilestdout 与 stderr 分别重定向到日志文件
-pidfilepid 记录位置,供 -stop 与监控使用
DemoDaemon服务主类,必须放在最后一个参数位置

5.2 进程验证

ps -ef | grep jsvc | grep -v grep

回显(实测,root 控制进程加 appuser JVM 子进程):

root        6749       1  0 07:51 ?        00:00:00 jsvc.exec -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon -user appuser -Xms256m -Xmx512m -outfile /opt/demo-daemon/demo-stdout.log -errfile /opt/demo-daemon/demo-stderr.log -pidfile /opt/demo-daemon/demo-daemon.pid DemoDaemon
appuser     6750    6749  1 07:51 ?        00:00:00 jsvc.exec -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon -user appuser -Xms256m -Xmx512m -outfile /opt/demo-daemon/demo-stdout.log -errfile /opt/demo-daemon/demo-stderr.log -pidfile /opt/demo-daemon/demo-daemon.pid DemoDaemon
jps -l

回显(实测):

6779 jdk.jcmd/sun.tools.jps.Jps
6750 DemoDaemon

jps 显示 JVM 子进程主类为 DemoDaemon,pid 6750 与 pidfile 一致(CentOS 8 的 jps 需用完整路径 /usr/lib/jvm/java-17-openjdk/bin/jps -l)。

5.3 日志验证

cat /opt/demo-daemon/demo-stdout.log

回显(实测):

[DemoDaemon] init() called, args=[]
[DemoDaemon] start() called, worker thread starting
[DemoDaemon] heartbeat #1, thread=demo-worker

init 与 start 依次执行、心跳线程已按 5 秒周期输出。堆参数生效性可用 jcmd 复核(实测 InitialHeapSize 268435456 即 256MB,MaxHeapSize 536870912 即 512MB):

jcmd $(cat /opt/demo-daemon/demo-daemon.pid) VM.flags | tr ' ' '\n' | grep -E 'InitialHeapSize|MaxHeapSize'

回显(实测):

-XX:InitialHeapSize=268435456
-XX:MaxHeapSize=536870912

六、优雅停止验证

sudo /usr/local/bin/jsvc -stop -pidfile /opt/demo-daemon/demo-daemon.pid DemoDaemon
echo RC=$?

回显(实测):

RC=0

注意 -stop 必须带类名,实测缺类名时直接报 No class specified / Cannot parse command line arguments(返回码 1)。停止后查看日志尾部确认生命周期走完:

tail -4 /opt/demo-daemon/demo-stdout.log

回显(实测):

[DemoDaemon] heartbeat #6, thread=demo-worker
[DemoDaemon] stop() called, notifying worker to exit
[DemoDaemon] worker thread exited
[DemoDaemon] destroy() called, resources released
jps -l
ls -l /opt/demo-daemon/demo-daemon.pid

回显(实测,进程消失且 pidfile 被自动删除):

6848 jdk.jcmd/sun.tools.jps.Jps
ls: cannot access '/opt/demo-daemon/demo-daemon.pid': No such file or directory

至此 jsvc 链路闭环完成:预检查、启动降权、进程与日志双重验证、优雅停止、pidfile 清理。

七、systemd 单元托管同一服务

不经过 jsvc,让 systemd 直接托管同一形态的心跳服务。该形态使用普通 main() 方法加关机钩子实现(Daemon 接口需要 jsvc 的 DaemonLoader 驱动,脱离 jsvc 时直接实现它不会被调用)。

7.1 编写普通 Java 服务

创建 /opt/demo-daemon/DemoMain.java:

public class DemoMain {

    private static volatile boolean running = true;

    public static void main(String[] args) throws Exception {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("[DemoMain] shutdown hook triggered, cleaning up");
            running = false;
        }, "demo-shutdown"));
        System.out.println("[DemoMain] main() started, args=" + java.util.Arrays.toString(args));
        int count = 0;
        while (running) {
            Thread.sleep(5000);
            count++;
            System.out.println("[DemoMain] heartbeat #" + count);
        }
        System.out.println("[DemoMain] main() exited");
    }
}
cd /opt/demo-daemon && sudo javac -encoding UTF-8 DemoMain.java
timeout 12 java -cp /opt/demo-daemon DemoMain | head -4

回显(实测,12 秒后由 timeout 发送 SIGTERM 触发关机钩子):

[DemoMain] main() started, args=[]
[DemoMain] heartbeat #1
[DemoMain] heartbeat #2
[DemoMain] shutdown hook triggered, cleaning up

7.2 编写并启用 systemd 单元

创建 /etc/systemd/system/demo-plain.service(每条 Environment 指令独占一行):

[Unit]
Description=DemoMain Service (plain java + systemd)
After=network.target

[Service]
Type=simple
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java -Xms256m -Xmx512m -cp /opt/demo-daemon DemoMain
SuccessExitStatus=143
Restart=on-failure

[Install]
WantedBy=multi-user.target
配置项说明
Type=simplejava 不 fork,systemd 直接跟踪主进程
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64锁定 JDK 17 运行时
ExecStart直接运行 java,堆 256 至 512MB
SuccessExitStatus=143JVM 收到 SIGTERM 走完关机钩子后以 143 退出,不声明该值停止后单元会进入 failed 状态(实测踩坑)
Restart=on-failure异常退出自动拉起
WantedBy=multi-user.target开机自启的挂载点

启用并启动(每条 systemctl 命令只操作一个单元):

sudo systemctl daemon-reload
sudo systemctl enable demo-plain.service
sudo systemctl start demo-plain.service
systemctl is-active demo-plain.service

回显(实测):

Created symlink /etc/systemd/system/multi-user.target.wants/demo-plain.service → /etc/systemd/system/demo-plain.service.
active

7.3 日志验证与停止

journalctl -u demo-plain.service --no-pager -n 3 | tail -3

回显(实测):

Oct 13 08:10:35 1a283e06d6da systemd[1]: Started DemoMain Service (plain java + systemd).
Oct 13 08:10:35 1a283e06d6da java[9705]: [DemoMain] main() started, args=[]
Oct 13 08:10:40 1a283e06d6da java[9705]: [DemoMain] heartbeat #1
sudo systemctl stop demo-plain.service
sleep 2
echo is_active=$(systemctl is-active demo-plain.service)
echo is_failed=$(systemctl is-failed demo-plain.service)

回显(实测):

is_active=inactive
is_failed=inactive

CentOS 8 差异:单元内 JAVA_HOME 与 ExecStart 的 JDK 路径写 /usr/lib/jvm/java-17-openjdk,实测行为一致。

八、两种托管形态对比

基于本文实测结果的关键差异:

维度jsvc 托管(第五、六章)systemd 托管(第七章)
进程形态控制进程 + JVM 子进程两级,ps 显示两个 jsvc.exec单个 java 进程,cgroup 直接跟踪
降权方式-user 参数,root 启动后降权User= 指令,systemd 完成切换
pid 管理-pidfile 显式落盘,含存活锁cgroup 托管,无 pidfile
优雅停止-stop 主动触发,走 stop 与 destroy 生命周期SIGTERM 触发关机钩子,需 SuccessExitStatus=143 配合
日志去向-outfile/-errfile 指定文件journald 收集(journalctl 查看)
自动重启-restarts(默认无限,间隔 60 秒)Restart= 策略
开机自启需另配 systemd 或 init 脚本包裹WantedBy= 即完成
额外依赖需编译 jsvc 原生二进制无原生依赖,仅需 JDK
特权端口降权绑定原生支持(配合 libcap 保留 NET_BIND_SERVICE 能力)需 CAP_NET_BIND_SERVICE 等附加配置

选型建议:需要 root 启动后降权绑定特权端口(如 80、443)、或希望服务代码与 Tomcat 生态保持同一 Daemon 抽象时选 jsvc;一般应用服务、希望把进程治理完全交给 systemd 时选纯 Java 加 systemd 形态,运维面更小。

九、参考链接

  1. Apache Commons Daemon 官方主页:https://commons.apache.org/proper/commons-daemon/
  2. jsvc 启动器文档(含参数说明):https://commons.apache.org/proper/commons-daemon/jsvc.html
  3. 官方下载目录(bin 包与 src 包):https://downloads.apache.org/commons/daemon/
  4. Daemon 接口 Javadoc:https://commons.apache.org/proper/commons-daemon/apidocs/org/apache/commons/daemon/Daemon.html
  5. Maven Central 上的 commons-daemon 构件:https://repo1.maven.org/maven2/commons-daemon/commons-daemon/
  6. systemd 单元配置官方手册:https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html