Java 双服务离线编译、依赖加密与发布流程
本文记录一套适用于定制 Linux 环境的 Java 构建与发布方案:不安装系统软件、不修改系统软件源、不污染用户 Maven 仓库,在仓库内固定 JDK、Maven 和加密工具版本,只构建指定的两个 Spring Boot 服务,并将共享依赖加密后原子发布。
一、背景与目标
目标服务器通常不能稳定访问公网,也不适合通过 yum、apt 或替换软件源来升级 Java、Maven、Git 等基础工具。对于已经交付的定制系统,修改系统环境还可能影响同机运行的其他程序。
因此,这套方案遵循以下原则:
- 构建工具随项目固定,不依赖服务器预装版本。
- 工具链解压到项目缓存,不写
/usr、/opt等系统目录。 - Maven 使用项目独立的本地仓库,不修改或污染
~/.m2。 - 加密工具优先使用仓库内置源码归档,不要求构建机安装 Git,也不在脚本中保存凭据。
- 只构建
cygcenterweb和cygsignsrvweb,避免产生无关服务。 - 先在临时目录完成编译、收集、加密和校验,全部成功后再替换正式产物。
这里的“离线”需要分成两层理解:
- 工具链离线:JDK、Maven 和加密工具源码都由仓库提供,不访问公网。
- 业务依赖解析:如果项目独立 Maven 仓库还是空的,第一次构建仍需要访问可用的内网 Maven 制品库。缓存完整后才可以在断网环境重复构建。
二、整体流程
构建脚本位于:
src_java/package/build.sh最终产物位于:
binary/java/
├── bin/
│ ├── javacontrol.sh
│ ├── cygcenterweb.sh
│ └── cygsignsrvweb.sh
└── jar/x64/
├── cygcenterweb.jar
├── cygsignsrvweb.jar
└── libs/
├── cygcenterweb-requirements.txt
├── cygsignsrvweb-requirements.txt
└── *.jar三、构建目录设计
构建相关目录如下:
src_java/package/
├── build.sh
├── README.md
├── settings.xml
├── toolchain.env
├── runtime/
│ ├── javacontrol.sh
│ ├── cygcenterweb.sh
│ └── cygsignsrvweb.sh
└── toolchains/
├── openjdk-8u171-linux-x64.tar.gz
├── apache-maven-3.6.3-bin.tar.gz
└── sunri-jar-encrypt-fe837986.tar.gz执行后还会出现本地缓存:
src_java/package/.cache/
├── downloads/
├── toolchains/
├── m2-repository/
└── work/.cache/ 只服务于当前工作区,不应提交版本管理。它包含解压后的工具链、独立 Maven 仓库和单次构建临时目录。
把 JDK 和 Maven 归档提交到项目会增大仓库体积,但对于无法访问公网、又不能调整系统环境的构建服务器,这是更可控的选择。后续如果有稳定的内网制品库,也可以把归档迁移到制品库,但必须继续保留版本锁定和 SHA256 校验。
四、固定构建输入
所有关键版本集中在 toolchain.env 中,不由构建机当前环境决定:
| 组件 | 固定版本 | SHA256 |
|---|---|---|
| OpenJDK | 8u171 | 1971795369e7a34da2adf95c7f7104f27fd6efbab70c13d53c37a409dbc5b078 |
| Maven | 3.6.3 | 26ad91d751b3a9a53087aefa743f4e16a17741d3915b219cf74112bf87a438c5 |
| 加密工具源码 | commit fe837986c0b0bc1d045e690a0ecdc9d2b3d1d3dc | 6818958569790c2a80f462f68240a1ad87139453ec52f4b9286461f1203bac5d |
spring-core 补丁 | 5.2.7.RELEASE | 1e02155649314ec85f344c11f2df05c4267f4213a39371858f5e769f2d26a9b2 |
spring-boot-loader-tools 补丁 | 2.3.1.RELEASE | 162ea2bc67ed8f20eaf8f1904382a8658ecac3bdb072cdb24e1ed6e0f7c0dd56 |
构建前可先做轻量检查:
cd src_java/package
./build.sh --check该命令会检查三个内置归档是否存在,并验证 SHA256,不执行 Maven 编译,也不修改正式输出目录。
固定版本不只是为了兼容旧代码。加密链路同时涉及 Spring Core、Spring Boot Loader、自定义类加载器和原生密码库,其中任何一个组件被 Maven 动态替换,都可能出现“编译成功但运行时无法解密”的问题。
五、工具链选择顺序
5.1 JDK
JDK 的选择顺序为:
- 显式配置的
BUILD_JAVA_HOME。 - 仓库内置的 OpenJDK 8u171 归档。
- 系统中恰好满足要求的完整 JDK 8u171。
- 配置的归档下载地址。
脚本不仅检查 bin/java,还检查 bin/javac。这是为了防止解压定位时误选中 jre/bin/java,最终把 JRE 当成完整 JDK,导致 Maven 编译报错:
javac: invalid target release: 8正确的判断标准是:
test -x "$JAVA_HOME/bin/java"
test -x "$JAVA_HOME/bin/javac"
"$JAVA_HOME/bin/java" -version
"$JAVA_HOME/bin/javac" -version5.2 Maven
Maven 的选择顺序为:
- 显式配置的
BUILD_MAVEN_HOME。 - 仓库内置的 Maven 3.6.3 归档。
- 配置的归档下载地址。
脚本通过以下参数隔离 Maven 配置和本地仓库:
mvn \
-s src_java/package/settings.xml \
-Dmaven.repo.local=src_java/package/.cache/m2-repository因此,替换 Spring Boot Loader 的操作只发生在项目缓存中,不会覆盖构建用户 ~/.m2 里的文件,也不需要执行备份和还原共享仓库的逻辑。
如确实需要使用构建机已有工具,可显式指定:
export BUILD_JAVA_HOME=/path/to/jdk8u171
export BUILD_MAVEN_HOME=/path/to/apache-maven-3.6.3指定后仍会做版本与可执行文件检查,不满足条件会立即终止。
六、加密工具的准备方式
旧流程在每次构建时从私有 Git 仓库克隆加密工具,非交互发布环境经常出现:
fatal: could not read Username for 'http://example.internal': terminal prompts disabled新流程将固定 commit 的源码制作成归档:
toolchains/sunri-jar-encrypt-fe837986.tar.gz脚本先校验归档 SHA256,再解压到本次构建临时目录并使用固定 Maven 编译。这样默认流程不依赖 Git,也不需要用户名、密码或 Token。
只有内置归档缺失时,脚本才启用网络克隆备用分支。凭据必须通过发布系统的密钥变量注入:
export ENCRYPT_GIT_USER='<只读账号>'
export ENCRYPT_GIT_TOKEN='<只读 Token>'
export ENCRYPT_REPO_URL='http://<内部代码平台>/<group>/<repo>.git'不要把账号或 Token 拼接进仓库 URL,也不要写入 build.sh、toolchain.env、CI 日志或博客文档。
加密工具编译完成后,需要取得三个文件:
cygjarenctool-jar-with-dependencies.jar
spring-core-5.2.7.RELEASE.jar
spring-boot-loader-tools-2.3.1.RELEASE.jar其中后两个文件必须再次校验 SHA256,防止私有工具源码或 lib/ 内容被无意替换。
七、为什么要替换 Spring Boot Loader
这套加密机制不是单纯给最终业务 Jar 加一个密码,而是修改依赖 Jar 内容,并依靠配套的类加载链路在运行时读取。构建出的可执行 Jar 必须使用与加密机制匹配的 Spring Boot Loader。
脚本先把官方 spring-boot-loader-tools:2.3.1.RELEASE 的 POM 和目录结构解析到独立 Maven 仓库,再用加密工具提供的同名补丁 Jar 覆盖该缓存中的 Loader:
.cache/m2-repository/
└── org/springframework/boot/spring-boot-loader-tools/2.3.1.RELEASE/
├── spring-boot-loader-tools-2.3.1.RELEASE.pom
└── spring-boot-loader-tools-2.3.1.RELEASE.jar这样,后续业务模块执行 Spring Boot repackage 时会使用匹配的 Loader,同时不会影响机器上的其他 Maven 项目。
spring-core-5.2.7.RELEASE.jar 也必须加入最终共享依赖目录。它与 Loader 补丁、加密后的依赖和运行时原生库属于同一套协议,不能随意只升级其中一个。
八、只构建两个服务
项目源码是多模块 Maven Reactor,但当前发布只需要:
| Maven 模块 | 最终服务 |
|---|---|
:sunri-service-auth-standalone | cygsignsrvweb.jar |
:sunri-web-center-starter | cygcenterweb.jar |
核心构建命令为:
mvn -ff clean install -DskipTests -T 1C \
-pl :sunri-service-auth-standalone,:sunri-web-center-starter \
-am参数含义:
-ff:第一个模块失败后立即停止,避免继续产生不完整产物。clean install:清理并构建 Reactor,同时把中间模块安装到项目独立 Maven 仓库。-DskipTests:跳过测试执行,但测试代码仍可能参与编译;这是当前项目既有策略。-T 1C:按 CPU 核数并行构建。-pl:只选择两个目标服务。-am:同时构建两个服务依赖的上游模块。
-am 很关键。它不会把所有业务服务都发布出来,只会补齐目标服务在当前 Reactor 中依赖的模块。
九、依赖收集与冲突检查
两个服务编译后,各自的 target/lib/ 中都有运行依赖。脚本将两组依赖合并到同一个 libs/ 目录,以减少重复文件,同时分别生成依赖清单:
cygcenterweb-requirements.txt
cygsignsrvweb-requirements.txt每个清单只记录该服务需要的 Jar 文件名。例如:
HikariCP-3.4.5.jar
jackson-core-2.11.0.jar
spring-context-5.2.7.RELEASE.jar合并时不能简单使用 cp -f 覆盖同名文件。脚本采用以下规则:
- 文件名不存在时直接复制。
- 同名且二进制内容一致时复用已有文件。
- 同名但内容不一致时立即失败。
- requirements 排序并去重。
- 加密后再次检查清单中的每个文件是否存在。
这可以提前发现最危险的一类问题:两个服务引用了名称相同、实际内容不同的依赖。如果直接覆盖,最后一个复制的文件会让另一个服务在运行时表现异常,而且问题很难从构建日志中发现。
当前一次成功构建的参考数据为:
cygcenterweb requirements: 255
cygsignsrvweb requirements: 111
共享依赖 Jar: 307
javac: 1.8.0_171数量可用于快速发现依赖大幅漂移,但不能作为绝对不变的验收条件。真正的验收条件是构建脚本中的完整性、冲突和加密结果检查。
十、共享依赖加密
服务 Jar 和依赖收集完成后,脚本先记录所有依赖 Jar 的 SHA256,然后执行:
java -Dfile.encoding=UTF-8 \
-jar cygjarenctool-jar-with-dependencies.jar \
<临时输出目录>/libs enc加密工具依赖指定版本的 C++ 运行库和国密原生库。默认路径为:
/opt/cygsys/lib/libstdc++.so.6
/home/sunri/gcc750_x64/binary/release/lib/libCygCryptogmssl.so构建时设置:
export LD_PRELOAD=/opt/cygsys/lib/libstdc++.so.6
export LD_LIBRARY_PATH=/home/sunri/gcc750_x64/binary/release/lib:/opt/cygsys/lib如果目标环境目录不同,可以覆盖:
export CYGSYS_LIB_DIR=/custom/cygsys/lib
export CYG_NATIVE_LIB_DIR=/custom/native/lib加密完成后,脚本比较加密前后的 SHA256,至少有一个依赖发生变化才允许继续。这样可以避免加密工具返回成功,但实际上没有处理任何文件的假成功。
需要特别注意:当前流程加密的是 libs/ 中的共享依赖,服务 Jar 则通过配套 Loader 和外部依赖加载机制启动。不要擅自改成只加密服务 Jar,也不要跳过 spring-core 补丁。
十一、原子发布
编译和加密不直接写正式目录,而是先写入类似下面的单次临时目录:
src_java/package/.cache/work/build.XXXXXX/output/java/jar/x64/只有以下检查全部通过,才会发布:
- 两个服务 Jar 都存在。
- 最终服务 Jar 数量严格等于 2。
- requirements 不为空。
- requirements 引用的所有依赖都存在。
- 同名依赖不存在内容冲突。
- 加密确实改变了依赖 Jar。
发布时,脚本先把现有 binary/java/jar/x64 移到本次构建临时目录作为备份,再把完整的新目录一次性移动到目标位置。如果移动失败,会恢复旧目录。
三个启动脚本也先写成隐藏临时文件,再通过同目录移动完成替换。这样可以尽量避免发布过程中出现半新半旧的目录状态。
十二、构建与验收
12.1 执行构建
cd <项目根目录>/src_java/package
./build.sh --check
./build.sh构建脚本不需要 sudo,也不应该使用 sudo。首次构建如果 Maven 缓存为空,需要保证 settings.xml 指向的内网 Maven 仓库可访问。
12.2 检查版本和产物
src_java/package/.cache/toolchains/jdk-8u171/bin/java -version
src_java/package/.cache/toolchains/jdk-8u171/bin/javac -version
src_java/package/.cache/toolchains/apache-maven-3.6.3/bin/mvn -version
find binary/java/jar/x64 -maxdepth 1 -type f -name '*.jar' -printf '%f\n' | sort
find binary/java/jar/x64/libs -maxdepth 1 -type f -name '*.jar' | wc -l
wc -l binary/java/jar/x64/libs/*-requirements.txt服务 Jar 应严格只有:
cygcenterweb.jar
cygsignsrvweb.jar12.3 校验 requirements
for list in binary/java/jar/x64/libs/*-requirements.txt; do
while IFS= read -r jar; do
test -f "binary/java/jar/x64/libs/$jar" || echo "缺失: $jar"
done < "$list"
done没有输出表示清单引用完整。
12.4 检查可执行 Jar
部分 Spring Boot executable Jar 带有前置启动脚本,unzip 可能提示存在前置字节。只要完整性检查最终退出码为 0,就不代表 Jar 已损坏:
unzip -tqq binary/java/jar/x64/cygcenterweb.jar
echo $?
unzip -tqq binary/java/jar/x64/cygsignsrvweb.jar
echo $?十三、部署与启动
假设部署根目录为:
/home/sunri/PRS7012-AMP/01_开发库/V1.00先配置 PRJHOME:
export PRJHOME=/home/sunri/PRS7012-AMP/01_开发库/V1.00需要长期生效时,应写入发布系统为该服务提供的环境变量配置,或写入部署用户自己的 Shell 初始化文件。不要修改全局 /etc/profile,除非该机器的运维规范明确要求。
启动两个服务:
cd "$PRJHOME/binary/java/bin"
./cygsignsrvweb.sh start
./cygcenterweb.sh start常用操作:
./cygcenterweb.sh stop
./cygcenterweb.sh restart
./cygcenterweb.sh start_clistart_cli 在前台运行,适合第一次部署时直接观察异常。后台启动日志默认位于:
/home/sunri/CygLog/cygcenterweb.console.log
/home/sunri/CygLog/cygsignsrvweb.console.log日志目录可通过 CYG_JAVA_LOG_DIR 覆盖。
13.1 启动脚本的职责
服务脚本只负责给出服务 Jar、动作和最大内存:
cygcenterweb.sh默认最大堆为 512 MB。cygsignsrvweb.sh默认最大堆为 256 MB。cygcenterweb.sh额外提供当前服务需要的安全与跨域参数。
真正的通用逻辑位于 javacontrol.sh:
- 根据
PRJHOME或CYGHOME定位 Java 发布目录。 - 检查服务 Jar、
libs/和对应 requirements。 - 检查国密库和兼容版
libstdc++.so.6。 - 优先使用
JAVA_HOME/bin/java,否则查找系统 Java。 - 根据服务器内存计算
-Xms和-Xmx。 - 设置
-Dloader.path=<libs>加载共享依赖。 - 设置原生库环境变量并启动、停止或重启进程。
可在启动时覆盖单个服务的 JVM 参数:
export cygcenterweb_opts='-Dsecurity.cipher.enable=false -Dsecurity.cors.enable=false -Dexample=value'
./cygcenterweb.sh restart也可以传入最大堆内存,单位为 MB:
./cygcenterweb.sh start 1024十四、常见故障排查
14.1 Maven 或 JDK 下载超时
现象:
curl: (28) Connection timed out
SHA256 不匹配先运行:
./build.sh --check如果内置归档完整,默认不会下载 JDK 和 Maven。若仍发生下载,说明对应归档缺失、文件名不匹配或校验失败,不应通过放宽校验来绕过,应恢复正确归档。
14.2 加密仓库要求输入账号
现象:
terminal prompts disabled默认构建不应访问 Git。检查:
ls -l toolchains/sunri-jar-encrypt-fe837986.tar.gz
./build.sh --check只有明确启用网络备用方案时,才通过发布平台密钥变量提供只读账号和 Token。
14.3 invalid target release: 8
确认 JAVA_HOME 指向 JDK 根目录而不是其中的 jre/:
"$JAVA_HOME/bin/java" -version
"$JAVA_HOME/bin/javac" -version两条命令都必须存在,版本应为 1.8.0_171。
14.4 Maven 业务依赖下载失败
工具链内置不等于所有业务依赖都已内置。检查内网 Maven 仓库:
curl -I http://<内网制品库>/repository/maven-public/同时检查 settings.xml 的镜像配置和发布环境注入的 MAVEN_REPOSITORY_URL。不要改成不受控的公网镜像来临时通过构建,否则依赖内容可能漂移。
14.5 同名依赖内容冲突
现象:
[java-build] 错误: 同名依赖内容冲突: example.jar这说明两个服务产出了文件名相同但内容不同的 Jar。应该回到 Maven 依赖树统一版本:
mvn dependency:tree不要修改脚本让后一个文件覆盖前一个文件。
14.6 国密原生库加载失败
先检查文件和动态依赖:
test -f /home/sunri/gcc750_x64/binary/release/lib/libCygCryptogmssl.so
test -f /opt/cygsys/lib/libstdc++.so.6
ldd /home/sunri/gcc750_x64/binary/release/lib/libCygCryptogmssl.so如果路径与服务器实际目录不同,通过 CYG_NATIVE_LIB_DIR 和 CYGSYS_LIB_DIR 覆盖。不要仅在交互 Shell 中临时设置后就认为发布系统也能读取;服务启动进程必须继承这些变量。
14.7 提示 PRJHOME 未配置
现象:
错误: PRJHOME 未配置设置并验证:
export PRJHOME=/home/sunri/PRS7012-AMP/01_开发库/V1.00
test -f "$PRJHOME/binary/java/jar/x64/cygcenterweb.jar"发布平台启动服务时,要在平台的环境变量配置中设置,而不是只写入某个交互终端的临时环境。
14.8 服务启动后立即退出
使用前台模式获取完整异常:
cd "$PRJHOME/binary/java/bin"
./cygcenterweb.sh start_cli或者查看后台日志:
tail -n 200 /home/sunri/CygLog/cygcenterweb.console.log排查顺序建议为:Java 版本、PRJHOME、服务 Jar、requirements、共享依赖、原生库、外部数据库/Redis/MQ 配置。
十五、安全与维护注意事项
- JDK、Maven、加密源码或补丁升级时,必须同步更新 SHA256,并在干净缓存中完成一次全量构建和启动验证。
- Spring Core、Spring Boot Loader、加密工具、自定义类加载器和原生密码库应作为一个兼容性组合管理,不能各自独立升级。
- 私有仓库凭据只能由发布平台密钥管理注入,不能提交到 Git。
src_java/package/.cache/和binary/java/属于生成内容,不应因本地构建被误提交。- 每次发布应保留构建日志、源码 commit、工具链版本和产物校验值,便于定位线上版本。
- 修改依赖收集逻辑后,必须同时验证两个 requirements,避免某个服务在共享目录中偶然依赖了另一个服务带入的 Jar。
- 生产启动前至少执行一次
start_cli,确认类加载器、国密原生库和外部配置链路都正常。
十六、一次完整操作示例
构建机执行:
cd <项目根目录>/src_java/package
./build.sh --check
./build.sh
find ../../binary/java/jar/x64 -maxdepth 1 -name '*.jar' -printf '%f\n' | sort
wc -l ../../binary/java/jar/x64/libs/*-requirements.txt目标服务器执行:
export PRJHOME=/home/sunri/PRS7012-AMP/01_开发库/V1.00
test -f /opt/cygsys/lib/libstdc++.so.6
test -f /home/sunri/gcc750_x64/binary/release/lib/libCygCryptogmssl.so
cd "$PRJHOME/binary/java/bin"
./cygsignsrvweb.sh start_cli认证服务前台验证通过后停止,再按相同方式验证中心服务,最后切换为后台启动:
./cygsignsrvweb.sh start
./cygcenterweb.sh start至此,构建机工具链、Maven 依赖、加密产物、共享依赖清单、原生库和运行时启动链路均完成闭环验证。
