Ubuntu 22.04 安装 Apache Commons Daemon 指南
在 Ubuntu 22.04 上从零安装 Apache Commons Daemon 1.6.1:安装 JDK 17 与编译工具,下载校验发行包,源码编译 jsvc,实测最小示例服务与 systemd 托管,并覆盖多版本管理与完整卸载流程。
Ubuntu 22.04 安装 Apache Commons Daemon 指南
目标:在 Ubuntu 22.04 服务器上从零完成 Apache Commons Daemon 的安装与验证,覆盖 JDK 17 与编译工具安装、发行包下载校验、jsvc 源码编译、最小示例服务运行与两种托管方案对比,全部命令已在 Ubuntu 22.04 / CentOS 8(JDK 17 + Maven)实测通过。
一、环境信息
本文所有命令均在以下环境中实测执行并逐条记录回显,撰写时以真实服务器视角给出。
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(x86_64) |
| JDK | OpenJDK 17.0.20.1(openjdk-17-jdk) |
| Maven | 3.6.3 |
| C 编译器 | gcc 11.4.0 |
| 构建工具 | GNU Make 4.3 |
| Apache Commons Daemon | 1.6.1(bin 包 + src 包) |
执行系统与硬件架构确认命令:
cat /etc/os-release | head -3
回显(实测):
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
uname -m
回显(实测):
x86_64
Apache Commons Daemon 由两部分组成:Java 支持库 commons-daemon-1.6.1.jar(提供 Daemon 接口)与原生启动器 jsvc(Linux 可执行文件,需从源码包编译)。bin 包内只有 jar 与文档,不包含任何 Linux 可执行文件,这是安装流程必须包含源码编译步骤的原因。
💡 技术拓展:把 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(方案 A 详解),systemd Unit 作为方案 B 对比讲解;Windows 平台对应的原生启动器是 prunsrv,本文不展开。
二、版本选型与方案对比
2.1 版本选型
截至本文实测日(2026-10-13),官方分发目录与 Maven Central 上可获取的近期版本如下(均为实测确认):
| 版本 | 发布物 | 获取渠道 | 实测说明 |
|---|---|---|---|
| 1.6.1 | bin 包 + src 包 | downloads.apache.org 在架 | 本文选型;jsvc 需从 src 包编译 |
| 1.6.0 | bin 包 + src 包 + Maven 构件 | downloads.apache.org 与 Maven Central | Maven Central 上 commons-daemon:commons-daemon 最新可用即 1.6.1,1.6.0 亦在架 |
| 1.5.1 | src 包 | 已从 downloads.apache.org 下架,仅存 Apache 归档站 | 实测主分发站 404,归档站可下载并校验通过 |
| 1.4.1 | bin 包 + src 包 | downloads.apache.org 与 Maven Central | 旧 LTS 常用版本 |
JDK 要求:源码包内 src/native/unix/INSTALL.txt 明确写出编译 jsvc 需要 Java 8 or later JDK(实测引用原文);本文 1.6.1 与 1.5.1 两个版本的 jsvc 均使用 JDK 17 编译通过,Java 支持库亦在 JDK 17 运行时验证通过。
本文选定 1.6.1,理由:官方分发站当前最新版,jsvc --help 已内建 Java 9 至 Java 17 的模块化与预览特性参数透传(实测输出可见 --add-modules、--enable-preview、--enable-native-access 等选项),对 JDK 17 时代的服务兼容性最好。
2.2 两种安装方案对比
| 维度 | 方案 A:jsvc 源码编译 | 方案 B:仅用 Java 程序 + systemd 托管 |
|---|---|---|
| 安装内容 | JDK + gcc/make + bin 包 jar + src 包编译出的 jsvc | 仅 JDK,无需编译任何原生代码 |
| Java 代码形态 | 实现 org.apache.commons.daemon.Daemon 接口 | 普通 main() 方法 + 关机钩子 |
| pid 管理 | -pidfile 指定,含存活检测与锁 | systemd cgroup 托管,无 pidfile |
| 降权运行 | -user 参数,root 启动后降权 | User= 指令 |
| 日志 | -outfile/-errfile 重定向到文件 | journald 收集 stdout |
| 优雅停止 | -stop 按 pidfile 通知,先 stop() 再 destroy() | SIGTERM 触发关机钩子,需配 SuccessExitStatus=143 |
| 是否依赖 systemd | 可脱离 systemd 单独运行 | 强依赖 systemd |
| 适用场景 | 需 root 启动绑定特权端口再降权、需统一 Daemon 接口抽象、与 Tomcat 生态一致的服务 | 无特权端口诉求、希望完全交给 systemd 管理的现代服务 |
选型建议:生产服务器同时具备 systemd 与降权绑定特权端口需求时优先方案 A;纯应用层服务、无 jsvc 维护诉求时方案 B 更简洁。两方案本文均完整实测,可按需选用其一。
三、安装 JDK 与编译工具
方案 A 需要编译工具链,方案 B 仅需 JDK 与 Maven(Maven 用于依赖管理,本指南一并安装)。执行安装:
sudo apt update
sudo apt install -y openjdk-17-jdk maven gcc make libc6-dev wget curl
回显(实测。全新机器会自动下载并解包大量依赖,本机此前已安装过,回显为已最新确认):
gcc is already the newest version (4:11.2.0-1ubuntu1).
make is already the newest version (4.3-4.1build1).
maven is already the newest version (3.6.3-5).
libc6-dev is already the newest version (2.35-0ubuntu3.15).
openjdk-17-jdk is already the newest version (17.0.20.1+1-1~22.04).
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
逐一验证安装结果:
java -version
回显(实测):
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
javac -version
回显(实测):
javac 17.0.20.1
mvn -version
回显(实测):
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"
gcc --version | head -1
回显(实测):
gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
make --version | head -1
回显(实测):
GNU Make 4.3
四、下载并校验发行包
创建统一存放目录并下载 bin 包(Java 支持库)与 src 包(jsvc 源码),同时下载官方 sha512 校验文件:
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
ls -l
回显(实测):
total 680
-rw-r--r-- 1 root root 282650 Oct 13 07:42 commons-daemon-1.6.1-bin.tar.gz
-rw-r--r-- 1 root root 162 Oct 13 07:42 commons-daemon-1.6.1-bin.tar.gz.sha512
-rw-r--r-- 1 root root 400630 Oct 13 07:42 commons-daemon-1.6.1-src.tar.gz
-rw-r--r-- 1 root root 162 Oct 13 07:42 commons-daemon-1.6.1-src.tar.gz.sha512
官方 sha512 文件内容为「裸哈希 + 空格 + 星号 + 文件名」格式,可直接交给 sha512sum -c 校验:
cat commons-daemon-1.6.1-bin.tar.gz.sha512
回显(实测):
4757860f7700908cd915141237321b2996ea07077066e8afaa1de322d9edbf7e20c73cb19c57e2bcc27e5e0756d7c3d568f8849718e2011e3b182a2420d7a969 *commons-daemon-1.6.1-bin.tar.gz
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
两个包都输出 OK 才可继续。解压两个包并确认 bin 包内容:
tar -xzf commons-daemon-1.6.1-bin.tar.gz
tar -xzf commons-daemon-1.6.1-src.tar.gz
ls commons-daemon-1.6.1
回显(实测):
LICENSE.txt
NOTICE.txt
RELEASE-NOTES.txt
commons-daemon-1.6.1-javadoc.jar
commons-daemon-1.6.1-sources.jar
commons-daemon-1.6.1-test-sources.jar
commons-daemon-1.6.1-tests.jar
commons-daemon-1.6.1.jar
可见 bin 包只有 Java 支持库 jar 与文档,没有任何 Linux 可执行文件,jsvc 必须按下一节从 src 包编译。
补充说明 Maven 依赖:若项目使用 Maven 管理该支持库,Maven Central 上可解析的坐标是旧 groupId commons-daemon:commons-daemon(1.6.1 实测可拉取,jar 与 bin 包内 jar 的 sha512 完全一致:3217edbd2458dbf1 开头);而 org.apache.commons:commons-daemon 在 Central 上仅有 2012 年的 1.0.9,直接引用会拉不到 1.6.1。
五、源码编译 jsvc
进入 src 包的原生代码目录,执行 configure 与 make。--with-java 必须显式指向 JDK 17 的安装目录(原因见 FAQ Q3):
cd /opt/commons-daemon/commons-daemon-1.6.1-src/src/native/unix
sudo ./configure --with-java=/usr/lib/jvm/java-17-openjdk-amd64
回显(实测节选,首尾完整保留关键行):
*** Current host ***
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking cached host system type... ok
*** C-Language compilation tools ***
checking for gcc... gcc
checking whether the C compiler works... yes
checking whether we are cross compiling... no
*** Java compilation tools ***
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 Makefile
config.status: creating Makedefs
config.status: creating native/Makefile
*** All done ***
Now you can issue "make"
WARNING: cannot find headers for libcap 是可选能力库缺失的提示,不影响编译与基本运行,处理办法见 FAQ Q1。执行编译:
sudo make
回显(实测):
(cd native; make all)
make[1]: Entering directory '/opt/commons-daemon/commons-daemon-1.6.1-src/src/native/unix/native'
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c jsvc-unix.c -o jsvc-unix.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c arguments.c -o arguments.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c debug.c -o debug.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c dso-dlfcn.c -o dso-dlfcn.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c dso-dyld.c -o dso-dyld.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c help.c -o help.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c home.c -o home.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c java.c -o java.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c location.c -o location.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c replace.c -o replace.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c locks.c -o locks.o
gcc -g -O2 -DOS_LINUX -DDSO_DLFCN -DCPU=\"amd64\" -Wall -Wstrict-prototypes -I/usr/lib/jvm/java-17-openjdk-amd64/include -I/usr/lib/jvm/java-17-openjdk-amd64/include/linux -c signals.c -o signals.o
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'
确认产物并安装到 PATH 目录:
ls -l jsvc
回显(实测):
-rwxr-xr-x 1 root root 197032 Oct 13 07:43 jsvc
sudo mkdir -p /opt/commons-daemon/jsvc-1.6.1
sudo cp jsvc /opt/commons-daemon/jsvc-1.6.1/jsvc
sudo ln -sf /opt/commons-daemon/jsvc-1.6.1/jsvc /usr/local/bin/jsvc
ls -l /usr/local/bin/jsvc
回显(实测):
lrwxrwxrwx 1 root root 35 Oct 13 07:57 /usr/local/bin/jsvc -> /opt/commons-daemon/jsvc-1.6.1/jsvc
/usr/local/bin/jsvc --help | tail -2
回显(实测):
jsvc (Apache Commons Daemon) 1.6.1
Copyright (c) 1999-2026 Apache Software Foundation.
版本横幅出现即编译安装成功。注意 jsvc 必须以绝对路径或相对路径调用(直接敲 jsvc 会被自身的 re-exec 机制拒绝,详见 FAQ Q4)。
六、方案 A:最小示例服务编译运行
6.1 准备运行目录与降权用户
sudo mkdir -p /opt/demo-daemon
sudo cp /opt/commons-daemon/commons-daemon-1.6.1/commons-daemon-1.6.1.jar /opt/demo-daemon/
sudo useradd -m -s /bin/bash appuser
id appuser
回显(实测):
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
6.2 编写示例服务
创建 /opt/demo-daemon/DemoDaemon.java,实现 Daemon 接口的四个生命周期方法,每 5 秒输出一次心跳:
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");
}
}
6.3 编译与预检查
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 解析源码,含中文注释会直接编译失败(详见 FAQ Q6)。用 jsvc 的 -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
6.4 启动、验证与停止
以 root 启动、-user appuser 降权运行,堆固定 256 至 512MB,日志与 pidfile 落在 /opt/demo-daemon:
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
等待约 6 秒后验证进程形态(root 控制进程 + appuser 运行的 JVM 子进程):
ps -ef | grep jsvc | grep -v grep
回显(实测):
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 从 JVM 视角确认主类:
jps -l
回显(实测):
6779 jdk.jcmd/sun.tools.jps.Jps
6750 DemoDaemon
检查 pidfile 与日志文件:
cat /opt/demo-daemon/demo-daemon.pid
回显(实测):
6750
cat /opt/demo-daemon/demo-stdout.log
回显(实测):
[DemoDaemon] init() called, args=[]
[DemoDaemon] start() called, worker thread starting
[DemoDaemon] heartbeat #1, thread=demo-worker
停止服务并验证优雅退出链路:
sudo /usr/local/bin/jsvc -stop -pidfile /opt/demo-daemon/demo-daemon.pid DemoDaemon
echo RC=$?
回显(实测):
RC=0
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
回显(实测):
6848 jdk.jcmd/sun.tools.jps.Jps
ls: cannot access '/opt/demo-daemon/demo-daemon.pid': No such file or directory
jps 中已无 DemoDaemon 进程,pidfile 在正常停止后被 jsvc 自动删除,生命周期方法按 stop、worker 退出、destroy 的顺序完整执行。注意 -stop 必须带上类名 DemoDaemon,只写 -stop -pidfile 会报 No class specified(参数解析要求)。
七、方案 B:纯 Java 程序加 systemd 托管
不使用 jsvc,用普通 main() 类 + 关机钩子 + systemd Type=simple 托管同一形态的服务。
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");
}
}
先以前台方式验证程序本身可运行(12 秒后由 timeout 发送 SIGTERM 触发关机钩子):
cd /opt/demo-daemon && sudo javac -encoding UTF-8 DemoMain.java
timeout 12 java -cp /opt/demo-daemon DemoMain | head -4
回显(实测):
[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 | 为 JVM 提供明确运行时,避免系统默认 JVM 漂移 |
| ExecStart | 直接运行 java,堆 256 至 512MB |
| SuccessExitStatus=143 | JVM 收到 SIGTERM 走完关机钩子后以退出码 143 结束,不声明该值单元会进入 failed 状态(实测踩坑) |
| Restart=on-failure | 异常退出时自动拉起 |
启动并验证:
sudo systemctl daemon-reload
sudo systemctl start demo-plain.service
sleep 6
systemctl is-active demo-plain.service
回显(实测):
active
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
停止并确认状态干净(不带 SuccessExitStatus=143 时实测 systemctl is-failed 会返回 failed):
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
八、多版本管理
利用第五节的目录约定,多版本 jsvc 共存只需按版本分目录放置二进制文件,再用软链接切换当前版本。先以 1.5.1 为例编译第二个版本(1.5.1 已从主分发站下架,需从 Apache 归档站获取):
cd /opt/commons-daemon
curl -fsSLO https://downloads.apache.org/commons/daemon/source/commons-daemon-1.5.1-src.tar.gz
回显(实测):
curl: (22) The requested URL returned error: 404
curl -fsSLO https://archive.apache.org/dist/commons/daemon/source/commons-daemon-1.5.1-src.tar.gz
curl -fsSLO https://archive.apache.org/dist/commons/daemon/source/commons-daemon-1.5.1-src.tar.gz.sha512
sha512sum -c commons-daemon-1.5.1-src.tar.gz.sha512
回显(实测):
commons-daemon-1.5.1-src.tar.gz: OK
tar -xzf commons-daemon-1.5.1-src.tar.gz
cd commons-daemon-1.5.1-src/src/native/unix
sudo ./configure --with-java=/usr/lib/jvm/java-17-openjdk-amd64 | tail -3
sudo make | tail -2
sudo mkdir -p /opt/commons-daemon/jsvc-1.5.1
sudo cp jsvc /opt/commons-daemon/jsvc-1.5.1/jsvc
ls -l /opt/commons-daemon/jsvc-1.5.1/
回显(实测):
config.status: creating native/Makefile
*** All done ***
Now you can issue "make"
gcc jsvc-unix.o libservice.a -ldl -lpthread -o ../jsvc
make[1]: Leaving directory '/opt/commons-daemon/commons-daemon-1.5.1-src/src/native/unix/native'
total 192
-rwxr-xr-x 1 root root 196504 Oct 13 07:58 jsvc
验证 1.5.1 的版本指纹,再切换软链接并分别确认:
/opt/commons-daemon/jsvc-1.5.1/jsvc -version -outfile '&1' -errfile '&2' -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon DemoDaemon | grep 'commons daemon'
回显(实测):
commons daemon version "1.5.1"
sudo ln -sf /opt/commons-daemon/jsvc-1.5.1/jsvc /usr/local/bin/jsvc
/usr/local/bin/jsvc -version -outfile '&1' -errfile '&2' -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon DemoDaemon | grep 'commons daemon'
回显(实测):
commons daemon version "1.5.1"
sudo ln -sf /opt/commons-daemon/jsvc-1.6.1/jsvc /usr/local/bin/jsvc
/usr/local/bin/jsvc -version -outfile '&1' -errfile '&2' -java-home /usr/lib/jvm/java-17-openjdk-amd64 -cp /opt/demo-daemon/commons-daemon-1.6.1.jar:/opt/demo-daemon DemoDaemon | grep -E 'commons daemon version'
回显(实测):
commons daemon version "1.6.1"
软链接指向哪个目录,/usr/local/bin/jsvc 就是哪个版本,升级与回滚都只需一条 ln -sf。
九、卸载
按依赖反向顺序清理:先停用并摘除 systemd 单元,再删除二进制与目录,最后删除降权用户。以下命令按本文方案 A 加方案 B 的完整安装路径编写,未启用对应单元时跳过相应条目即可:
sudo systemctl stop demo-daemon.service
sudo systemctl disable demo-daemon.service
stop 停止单元无回显,disable 摘除开机自启链接的回显(实测):
Removed /etc/systemd/system/multi-user.target.wants/demo-daemon.service.
sudo systemctl stop demo-plain.service
sudo rm -f /etc/systemd/system/demo-daemon.service /etc/systemd/system/demo-plain.service
sudo systemctl daemon-reload
systemctl is-active demo-daemon.service
回显(实测):
inactive
sudo rm -f /usr/local/bin/jsvc
sudo rm -rf /opt/commons-daemon /opt/demo-daemon
sudo userdel -r appuser
回显(实测):
userdel: appuser mail spool (/var/mail/appuser) not found
该提示仅表示 mail spool 文件本就不存在,用户与家目录已被删除。最终验证:
which jsvc
回显(实测:无任何输出,退出码为 1):
ls /opt/commons-daemon
回显(实测):
ls: cannot access '/opt/commons-daemon': No such file or directory
id appuser
回显(实测):
id: 'appuser': no such user
十、常见问题 FAQ
Q1:configure 阶段报 WARNING: cannot find headers for libcap,要紧吗?
不影响编译与常规运行,但会关闭 jsvc 的能力集(capabilities)支持。安装 libcap-dev 后重新 configure 即可启用:实测安装后该行变为 checking for sys/capability.h... yes,源码随之以 -DHAVE_LIBCAP 编译,jsvc 降权后将保留 CAP_NET_BIND_SERVICE、CAP_SETUID、CAP_SETGID、CAP_DAC_READ_SEARCH 等能力,使非 root 运行的服务仍能绑定 1024 以下特权端口。若服务需要绑定特权端口,务必装上它再编译。
Q2:执行 jsvc -version 没有任何输出?
-version 的输出发生在 jsvc 把 stdout/stderr 重定向到 -outfile/-errfile(默认 /dev/null)之后,因此默认会被吞掉。两种解法(均实测有效):显式传 -outfile '&1' -errfile '&2' 保持控制台输出;或加 -debug(调试模式下默认 errfile 为 /dev/null 时会跳过重定向)。另外 -version 与 -stop 一样必须带类名参数,否则报 No class specified。
Q3:运行时报 UnsupportedClassVersionError,提示 class file version 61.0 与 55.0 不匹配?
服务器上存在多个 JDK 且 jsvc 未拿到预期的 -java-home:实测 Ubuntu 22.04 装完依赖后 /usr/lib/jvm/default-java 软链接指向 Java 11,jsvc 在未显式指定 -java-home 且未设置 JAVA_HOME 环境变量时会落到 11 上。解法是所有 jsvc 命令都显式带 -java-home /usr/lib/jvm/java-17-openjdk-amd64,或在启动环境里 export JAVA_HOME。
Q4:直接执行 jsvc --help 报 JSVC re-exec requires execution with an absolute or relative path?
jsvc 启动时会 re-exec 自身,要求调用方给出路径;仅靠 PATH 解析出来的裸命令名不满足条件。统一用 /usr/local/bin/jsvc 这类绝对路径调用即可。
Q5:Maven 里应该用哪个坐标?
org.apache.commons:commons-daemon 在 Maven Central 上仅有 1.0.9(实测查询 metadata 确认),不要用它引 1.6.1。可用坐标是旧 groupId 的 commons-daemon:commons-daemon:1.6.1,实测 mvn dependency:get 可拉取,且其 jar 与官方 bin 包内 jar 的 sha512 完全一致(3217edbd2458dbf1 开头)。
Q6:javac 报大量 error: unmappable character (0xE6) for encoding US-ASCII?
系统 locale 为 C 时 javac 默认以 US-ASCII 读取源码,源码里的中文注释全部无法解析。编译命令加 -encoding UTF-8 即可,本文所有 javac 示例均已携带该参数。
十一、参考链接
- Apache Commons Daemon 官方主页:https://commons.apache.org/proper/commons-daemon/
- 官方下载目录(bin 包与 src 包):https://downloads.apache.org/commons/daemon/
- jsvc 启动器文档(含参数说明):https://commons.apache.org/proper/commons-daemon/jsvc.html
- Apache 归档站(历史版本下载):https://archive.apache.org/dist/commons/daemon/
- 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