Spring Boot Jar 内文件更新

在实际的项目维护中经常会出现临时应急的情况,比如对某个依赖升级,或者对功能代码微调更新,由于时间紧再去走打包构建流程可能来不及,这个时候就需要直接对实际项目进行替换更新操作。这里就来介绍下如何对 Spring Boot 打包后的 Jar 进行文件更新。

命令概览

关于 Jar 文件的调整修改主要使用 jar 命令进行操作,输入 jar 可以看到相关提示信息

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
$ jar
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:
-c 创建新档案
-t 列出档案目录
-x 从档案中提取指定的 (或所有) 文件
-u 更新现有档案
-v 在标准输出中生成详细输出
-f 指定档案文件名
-m 包含指定清单文件中的清单信息
-n 创建新档案后执行 Pack200 规范化
-e 为捆绑到可执行 jar 文件的独立应用程序
指定应用程序入口点
-0 仅存储; 不使用任何 ZIP 压缩
-P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
-M 不创建条目的清单文件
-i 为指定的 jar 文件生成索引信息
-C 更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
'm', 'f''e' 标记的指定顺序相同。

示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 'mymanifest'
将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
jar cvfm classes.jar mymanifest -C foo/ .

可以看到官方给了两个示例,在后面会给出更详细介绍说明。

操作示例

就目前工作中经常遇见的操作有,提取文件替换文件删除文件,下面就来举例一一介绍下相关的流程

提取文件

提取文件的主要目的是为了查看或是修改某些处理逻辑,一般是排查问题,或为增量修改做基础。命令为 jar xf xxx.jar [文件路径...],如果未指定文件路径,则解压全部文件。

例如我们需要查看内嵌 Jar 中的 application.yml 文件,则可以使用如下命令

1
2
3
4
5
# 定位文件路径
$ jar tvf xxx.jar | grep application.yml
395 Wed Jul 26 10:17:26 CST 2023 BOOT-INF/classes/config/application.yml
# 解压文件
$ jar xf xxx.jar BOOT-INF/classes/config/application.yml

查看文件内容 cat BOOT-INF/classes/config/application.yml

覆盖文件

该操作主要是用于更新 Jar 文件内的文件,例如 class 或是 内嵌的配置 证书之类的操作,jar u0f xxx.jar [文件路径...]

以替换上述的 application.yml 为例,

1
2
3
4
# 按文件路径替换
$ jar u0f xxx.jar BOOT-INF/classes/config/application.yml
# 按文件夹路径替换
$ jar u0f xxx.jar BOOT-INF/classes

由于替换会逐级读取替换路径下的文件,所以第二种做法也是可以的。

删除文件

删除文件相对较为麻烦,常见于漏洞修复中升级依赖等操作,例如升级有漏洞当 json 库或者 mysql 库等。

以升级有漏洞的 mysql 库为例

1
2
3
4
5
6
7
8
# 解压全部文件
$ jar xf xxx.jar
# 移除旧版 mysql
$ rm BOOT-INF/lib/mysql-connector-java-8.0.16.jar
# 复制无漏洞 mysql 库
$ mv mysql-connector-java-8.0.29.jar BOOT-INF/lib
# 重新压缩
$ jar c0Mf xxx.jar .

预处理

可能有些人会遇到 jar xf 没有效果的情况,这里就不得不提到 spring-boot-maven-plugin 参数配置,通读 Spring Boot 官方文档,可以在部署章节中看到 executable 配置,该配置用于生成可执行 Jar,允许我们直接用命令启动,同时支持软连接作为 init.d 服务启动。但这种方便是有代价的,此配置可能会导致 Jar 无法被正常的 jar xf,阻碍文件替换操作。实际上 Spring Boot 官方不建议大家设置 executable

但如果设置了还是容易被坑,所以在操作前还是判断这个下 Jar 是不是设置 executabletrue,有源码当情况可以直接看 pom.xml 中的值,没源码情况下也可输入 head xxx.jar,若是输出如下内容则意味着值被设置为 true

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
#
# . ____ _ __ _ _
# /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
# ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
# \\/ ___)| |_)| | | | | || (_| | ) ) ) )
# ' |____| .__|_| |_|_| |_\__, | / / / /
# =========|_|==============|___/=/_/_/_/
# :: Spring Boot Startup Script ::
#

此时候可以 grep 'exit 0' db-proxy.jar -an 查看嵌入脚本的结束行,一般输出如下结果

1
2
$ grep 'exit 0' xxx.jar -an
306:exit 0

这意味着此脚本在306行结束,用如下命令处理下jar文件

1
2
3
4
5
6
7
# 备份运行脚本
$ head -n 306 xxx.jar > run.sh
# 移除运行脚本
$ sed -i '1,306d' xxx.jar
# 执行文件替换操作
# 还原回去执行脚本
$ cp xxx.jar xxx.jar.tmp && cat run.sh xxx.jar.tmp > xxx.jar

执行上述操作后将会得到一个可操作的 Jar, 该 Jar 与未设置 executable 或设置为 false 一致,做完替换操作后再将 run.sh 还原回去即可。

写在最后

在更早之前 Tomcat 时代其实替换操作相对比较简单,步入 Spring Boot 时代之后更多的是需要面向 Jar 来操作,当然替换操作只能用来应急,最好还是要走项目放行流程跟踪稳妥。希望可以帮到大家。