使用 Java Service Wrapper 部署 Java 应用

Java Service Wrapper 是一个使 Java 应用程序能作为 Windows 服务或 UNIX / Linux 守护进程运行的开源软件。它提供了一个可靠的方式来启动、停止,并且监控 Java 应用程序的状态。

Java Service Wrapper(以下简称 JSW)的主要特点和功能包括:

  • 作为服务或守护进程运行:允许 Java 程序在没有用户登录情况下作为服务或守护进程运行。
  • 控制和管理:提供对 Java 应用程序的启动、停止、重启等控制管理功能。
  • 配置灵活:通过配置文件对 Java 虚拟机参数、环境变量、类路径等进行灵活配置。
  • 日志记录:支持详细的日志记录,便于问题排查和监控。
  • 性能监控:监控 Java 应用程序的性能,如响应时间和内存使用情况。
  • 错误恢复:可以配置服务在遇到错误时的自动恢复动作,如重新启动服务。
  • 跨平台:支持在 Windows、Linux 和其他 UNIX / Linux 系统上运行。

在工作中,我使用 JSW 将 Spring Boot 开发的 Java 应用部署在 Linux 服务器上,对于相同的平台环境可以做到一次部署,到处运行。

下载 JSW

截止到 2024 年 4 月,JSW 支持的系统和架构如下:

基本涵盖了主流的操作系统和架构,下载时选择与服务器对应的操作系统和架构。JSW 有三个版本,分别是专业版、标准版和社区版,我们一般下载社区版,社区版是免费使用的。以 Linux x86 64 位为例,下载最新版本的 .tar.gz 格式的压缩包,先解压出来看看文件目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
├── README_de.txt
├── README_en.txt
├── README_es.txt
├── README_ja.txt
├── bin
│   ├── demoapp
│   ├── testwrapper
│   └── wrapper
├── conf
│   ├── demoapp.conf
│   └── wrapper.conf
├── doc
│   ├── index.html
│   ├── revisions.txt
│   └── wrapper-community-license-1.3.txt
├── lib
│   ├── libwrapper.so
│   ├── wrapper.jar
│   ├── wrapperdemo.jar
│   └── wrappertest.jar
├── logs
│   └── wrapper.log
└── src
├── bin
│   ├── App.sh.in
│   └── App.shconf.in
└── conf
└── wrapper.conf.in

9 directories, 20 files

压缩包为我们提供了一个 demoapp 测试程序,你可以在终端进入到 bin 目录执行 ./testwrapper console 来测试一下 JSW 在当前系统是否正常工作。

JSW 的核心文件主要有五部分组成:

  • bin/wrapper wrapper 主程序,一个可执行的二进制文件
  • lib/libwrapper.so 本地库文件,使用此文件与操作系统交互
  • lib/wrapper.jar wrapper 使用此 jar 包实现 JVM 参数配置和日志记录等功能
  • src/bin/App.sh.in wrapper 的控制脚本,包括启动、停止等操作
  • src/conf/wrapper.conf.in wrapper 配置文件,包括 JVM 参数配置、启动类配置、启动参数等配置

部署 Spring Boot 项目

首先使用 maven 创建一个简单的 Spring Boot 项目jsw-demo 并添加 spring-boot-starter-web 依赖,项目文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│   ├── java
│   │   └── cc
│   │   └── cui
│   │   └── jswdemo
│   │   └── JswDemoApplication.java
│   └── resources
│   ├── application.yml
│   │   ├── static
│   │   │   └── a.txt
│   │   └── templates
│   │   └── b.txt
└── test
└── java
└── cc
└── cui
└── jswdemo
└── JswDemoApplicationTests.java

但默认的 maven 配置打包出来的是一个 jar 包,为了保持与官网 demo 相同的目录风格,我希望最终打包好的程序目录结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
jsw-demo/target/jsw-demo 
❯ tree
.
├── bin
│   ├── application.yml
│   ├── cc
│   │   └── cui
│   │   └── jswdemo
│   │   └── JswDemoApplication.class
│   ├── jsw-demo
│   ├── static
│   │   └── a.txt
│   ├── templates
│   │   └── b.txt
│   ├── wrapper
│   └── wrapper.conf
└── lib
├── jackson-annotations-2.15.4.jar
├── jackson-core-2.15.4.jar
├── jackson-databind-2.15.4.jar
├── jackson-datatype-jdk8-2.15.4.jar
├── jackson-datatype-jsr310-2.15.4.jar
├── jackson-module-parameter-names-2.15.4.jar
├── jakarta.annotation-api-2.1.1.jar
├── jul-to-slf4j-2.0.12.jar
├── libwrapper.so
├── log4j-api-2.21.1.jar
├── log4j-to-slf4j-2.21.1.jar
├── logback-classic-1.4.14.jar
├── logback-core-1.4.14.jar
├── micrometer-commons-1.12.4.jar
├── micrometer-observation-1.12.4.jar
├── slf4j-api-2.0.12.jar
├── snakeyaml-2.2.jar
├── spring-aop-6.1.5.jar
├── spring-beans-6.1.5.jar
├── spring-boot-3.2.4.jar
├── spring-boot-autoconfigure-3.2.4.jar
├── spring-boot-starter-3.2.4.jar
├── spring-boot-starter-json-3.2.4.jar
├── spring-boot-starter-logging-3.2.4.jar
├── spring-boot-starter-tomcat-3.2.4.jar
├── spring-boot-starter-web-3.2.4.jar
├── spring-context-6.1.5.jar
├── spring-core-6.1.5.jar
├── spring-expression-6.1.5.jar
├── spring-jcl-6.1.5.jar
├── spring-web-6.1.5.jar
├── spring-webmvc-6.1.5.jar
├── tomcat-embed-core-10.1.19.jar
├── tomcat-embed-el-10.1.19.jar
├── tomcat-embed-websocket-10.1.19.jar
└── wrapper.jar

在上述的文件结构中,target 目录下包含了一个 jsw-demo 的项目文件夹,其 bin 目录包含了 target/classes 目录下的所有文件,并额外增加了 wrapperwrapper.conf 文件,lib 目录包含了所有第三方依赖包和 libwrapper.so 本地库文件和 wrapper.jar 文件。

为了实现上面的打包效果,需要我们做一些额外的操作。首先需要将 JSW 的核心文件复制到项目中。

  • bin/wrapper –> ${APP_HOME}/wrapper/bin/
  • src/bin/App.sh.in –> ${APP_HOME}/wrapper/bin/jsw-demo (与项目名称一致或自定义)
  • src/bin/wrapper.conf.in –> ${APP_HOME}/wrapper/bin/wrapper.conf
  • lib/libwrapper.so –> ${APP_HOME}/wrapper/lib/
  • lib/wrapper.jar –> ${APP_HOME}/wrapper/lib/

此时项目文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.
├── main
│   ├── java
│   │   └── cc
│   │   └── cui
│   │   └── jswdemo
│   │   └── JswDemoApplication.java
│   ├── resources
│   │   ├── application.yml
│   │   ├── static
│   │   │   └── a.txt
│   │   └── templates
│   │   └── b.txt
│   └── wrapper
│   ├── bin
│   │   ├── jsw-demo
│   │   ├── wrapper
│   │   └── wrapper.conf
│   └── lib
│   ├── libwrapper.so
│   └── wrapper.jar
└── test
└── java
└── cc
└── cui
└── jswdemo
└── JswDemoApplicationTests.java

随后配置 maven 打包插件,将 src/wrapper 目录下的文件复制到对应的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 将依赖 copy 到/bin/lib/目录 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/jsw-demo/lib</outputDirectory>
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<id>copy-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>${project.build.directory}/jsw-demo/bin</outputDirectory>
<resources>
<!-- copy resources 目录 -->
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
<!-- copy wrapper bin 目录 -->
<resource>
<directory>${project.basedir}/src/main/wrapper/bin</directory>
</resource>
<resource>
<directory>${project.build.directory}/classes</directory>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-wrapper-lib</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<!-- 把 wrapper 依赖库 copy 到 lib 目录 -->
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>${project.build.directory}/jsw-demo/lib</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/wrapper/lib</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

在部署到服务器之前还需要更改 src/wrapper/bin 目录下的 jsw-demo 文件和 wrapper.conf 文件,来配置应用名称和启动方式等参数。

jsw-demo 实际上是 wrapper 的启动脚本,主要改动里面的几个地方:

1
2
3
4
5
6
7
# 定义 app 名称,可以保持一致
APP_NAME="jsw-demo"
APP_LONG_NAME="JSW Demo App"

# wrapper 和其配置文件路径,因为上面放在了同一个目录中,所以指定为当前目录
WRAPPER_CMD="./wrapper"
WRAPPER_CONF="./wrapper.conf"

wrapper.conf 是 wrapper 的配置文件,配置项较多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 定义 wrapper.jar 文件路径
wrapper.jarfile=../lib/wrapper.jar

# 定义 java 可执行文件绝对路径,当能在环境变量中找到 java 时直接赋值为 java 即可
# 如果环境变量中找不到 java,则需要配置绝对路径
wrapper.java.command=java
# 也可以配置 JAVA_HOME,和上面的方式二选一
set.JAVA_HOME=/java/path
wrapper.java.command=%JAVA_HOME%/bin/java

# 定义 wrapper 的集成方式,有四种,这里使用最简单的一种
# 参考:http://wrapper.tanukisoftware.com/doc/english/integrate.html
wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp

# 定义 Classpath,序号从 1 开始,要包含第三方 jar 包和 bin 目录
wrapper.java.classpath.1=../lib/*.jar
wrapper.java.classpath.2=../bin
# 定义 libwrapper.so 文件和第三方依赖包路径
wrapper.java.library.path.1=../lib

# JVM 堆内存初始值(MB)
wrapper.java.initmemory=500
# JVM 堆内存最大值(MB)
wrapper.java.maxmemory=2048

# 定义 Spring Boot 启动类路径,注意不是上面的 wrapper.java.mainclass
wrapper.app.parameter.1=cc.cui.jswdemo.JswDemoApplication

# 控制台日志格式定义
wrapper.console.format=PM
# 控制台日志级别
wrapper.console.loglevel=INFO
# 日志文件写入地址
wrapper.logfile=../logs/wrapper.log
# 日志文件写入格式
wrapper.logfile.format=LPTM
# 日志文件写入级别
wrapper.logfile.loglevel=INFO
# 定义日志文件滚动模式,当日志大小超过 wrapper.logfile.maxsize 时创建新的日志文件
# https://wrapper.tanukisoftware.com/doc/english/prop-logfile-rollmode.html
# SIZE: 当日志大小超过 wrapper.logfile.maxsize 时滚动
# WRAPPER: 当每次启动 wrapper 时滚动,此模式下日志文件大小会无限大,直到 wrapper 重启
# SIZE_OR_WRAPPER: 当日志大小超过 wrapper.logfile.maxsize 或每次启动 wrapper 时滚动
wrapper.logfile.rollmode=SIZE_OR_WRAPPER
# 日志文件最大值,10MB
wrapper.logfile.maxsize=10m
# 最大保留的日志文件数量,为 0 时表示不限制(不建议设置为 0)
wrapper.logfile.maxfiles=10
# 是否显示系统日志
wrapper.syslog.loglevel=NONE

到目前为止,wrapper 就集成到了 Spring Boot 项目中,使用 maven 打包后,把 target 下面的整个项目文件夹 jsw-demo 上传到服务器尝试运行。

在运行之前需要创建 wrapper.conf 文件中配置的日志目录,因为 wrapper 并不会自动创建,当 wrapper 找不到日志目录时,会将日志写入到与启动脚本相同的目录下。

上面说了 bin/jsw-demo 文件其实是一个启动脚本,赋予可执行权限并执行 ./jsw-demo console 以控制台方式启动 wrapper,如果配置正确的话应该会看到类似下面的结果:

可以使用 ./jsw-demo 命令查看启动脚本的帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost bin]#./jsw-demo 
Usage: ./jsw-demo [ console | start | stop | restart | condrestart | status | install | installstart | remove | dump ]

Commands:
console Launch in the current console.
start Start in the background as a daemon process.
stop Stop if running as a daemon or in another console.
restart Stop if running and then start.
condrestart Restart only if already running.
status Query the current status.
install Install to start automatically when system boots.
installstart Install and start running as a daemon process.
remove Uninstall.
dump Request a Java thread dump if running.
  • console 在当前 shell 终端启动
  • start 以守护进程在后台启动
  • stop 停止守护进程或停止在另一个终端启动的进程
  • restart 重启
  • condrestart 仅在运行时重启
  • status 查看当前运行状态
  • install 安装到当前系统并随机启动
  • installstart 安装到当前系统并马上启动(since v3.5.28)
  • remove 卸载
  • dump 如果进程已经启动,则请求 Java 线程转储
  1. 首次部署时需要手动创建 wrapper.conf 配置中的日志目录
  2. wrapper 启动后会在启动脚本同级目录创建 pid 等文件
  3. wrapper 停止后会删除创建的 pid 等文件

高级用法

实现 Java 应用自动重启

有时我们希望部署的 Java 应用在某些场景下能够自动重启,比如在工作中我使用 Java 编写了一个升级系统,当升级系统自身升级了依赖包之后,我希望它能自动重启以生效。此时我们可以借助 Java Service Wrapper 的特性实现。

wrapper.conf 底部增加配置:

1
2
# 自定义退出代码和操作
wrapper.on_exit.9=RESTART

上面的配置告诉 JSW,当程序的退出代码为 9 时执行重启操作。

随后需要在 Java 项目中合适的位置定义退出代码:

1
System.exit(9);

参考资料


使用 Java Service Wrapper 部署 Java 应用
https://cui.cc/java-service-wrapper/
作者
南山崔崔
发布于
2024年4月10日
许可协议