Ubuntu 22.04 安装 Apache Commons Daemon 指南

在 Ubuntu 22.04 上从零安装 Apache Commons Daemon 1.6.1:安装 JDK 17 与编译工具,下载校验发行包,源码编译 jsvc,实测最小示例服务与 systemd 托管,并覆盖多版本管理与完整卸载流程。

Apache Commons DaemonApache Commons Daemon2026/10/13

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)
JDKOpenJDK 17.0.20.1(openjdk-17-jdk)
Maven3.6.3
C 编译器gcc 11.4.0
构建工具GNU Make 4.3
Apache Commons Daemon1.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 管理降权运行日志信号处理开机自启
jsvcpidfile 记录-user 参数原生支持-outfile/-errfile 重定向内建优雅停机协议需配合 systemd 或 init 脚本
prunsrvWindows 服务管理器托管服务账号/凭据配置stdout/stderr 重定向到文件Windows 服务控制协议注册为 Windows 服务即自启
systemd Unitcgroup 全程托管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.1bin 包 + src 包downloads.apache.org 在架本文选型;jsvc 需从 src 包编译
1.6.0bin 包 + src 包 + Maven 构件downloads.apache.org 与 Maven CentralMaven Central 上 commons-daemon:commons-daemon 最新可用即 1.6.1,1.6.0 亦在架
1.5.1src 包已从 downloads.apache.org 下架,仅存 Apache 归档站实测主分发站 404,归档站可下载并校验通过
1.4.1bin 包 + 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=simplejava 进程本身不 fork,systemd 直接跟踪主进程
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64为 JVM 提供明确运行时,避免系统默认 JVM 漂移
ExecStart直接运行 java,堆 256 至 512MB
SuccessExitStatus=143JVM 收到 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 示例均已携带该参数。

十一、参考链接

  1. Apache Commons Daemon 官方主页:https://commons.apache.org/proper/commons-daemon/
  2. 官方下载目录(bin 包与 src 包):https://downloads.apache.org/commons/daemon/
  3. jsvc 启动器文档(含参数说明):https://commons.apache.org/proper/commons-daemon/jsvc.html
  4. Apache 归档站(历史版本下载):https://archive.apache.org/dist/commons/daemon/
  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