Apache Commons Daemon 快速入门与集成示例
从零演示 Apache Commons Daemon 集成全流程:编写实现 Daemon 接口的示例服务,编译后用 jsvc 以 256 至 512MB 堆启动并验证,-stop 优雅停止,并对比 systemd 托管形态。
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 LTS | CentOS Linux 8 |
| JDK | OpenJDK 17.0.20.1 | OpenJDK 17.0.1 |
| Maven | 3.6.3 | 3.5.4 |
| commons-daemon | 1.6.1 | 1.6.1 |
| jsvc | 源码编译产物(197032 字节) | 源码编译产物(199328 字节) |
约定:jsvc 安装在 /usr/local/bin/jsvc,工作目录 /opt/demo-daemon,降权用户 appuser。示例类名统一为 DemoDaemon。
💡 技术拓展:把 Java 程序做成守护进程有四种常见方案,选型对比如下。
方案 pid 管理 降权运行 日志 信号处理 开机自启 jsvc pidfile 记录 -user 参数原生支持 -outfile/-errfile 重定向 内建优雅停机协议 需配合 systemd 或 init 脚本 prunsrv Windows 服务管理器托管 服务账号/凭据配置 stdout/stderr 重定向到文件 Windows 服务控制协议 注册为 Windows 服务即自启 systemd Unit cgroup 全程托管 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 调用时机 | 本例行为 |
|---|---|---|
| init | JVM 创建后、服务生效前 | 保存 DaemonContext,打印启动参数 |
| start | init 成功后 | 启动 demo-worker 心跳线程 |
| stop | 收到 -stop 或 SIGTERM | 置位 running 并等待线程退出 |
| destroy | stop 完成后、进程退出前 | 打印资源释放日志 |
四、编译与预检查
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 appuser | root 完成启动后降权到 appuser 运行 JVM |
| -Xms256m -Xmx512m | 初始堆 256MB,最大堆 512MB |
| -outfile / -errfile | stdout 与 stderr 分别重定向到日志文件 |
| -pidfile | pid 记录位置,供 -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=simple | java 不 fork,systemd 直接跟踪主进程 |
| Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 | 锁定 JDK 17 运行时 |
| ExecStart | 直接运行 java,堆 256 至 512MB |
| SuccessExitStatus=143 | JVM 收到 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 形态,运维面更小。
九、参考链接
- Apache Commons Daemon 官方主页:https://commons.apache.org/proper/commons-daemon/
- jsvc 启动器文档(含参数说明):https://commons.apache.org/proper/commons-daemon/jsvc.html
- 官方下载目录(bin 包与 src 包):https://downloads.apache.org/commons/daemon/
- Daemon 接口 Javadoc:https://commons.apache.org/proper/commons-daemon/apidocs/org/apache/commons/daemon/Daemon.html
- Maven Central 上的 commons-daemon 构件:https://repo1.maven.org/maven2/commons-daemon/commons-daemon/
- systemd 单元配置官方手册:https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html