构建与部署制品:Maven、Gradle 与 Node 项目
源码目录里同时出现 pom.xml、package.json 和 Dockerfile 时,部署对象可能有三种:Java 可执行 JAR、浏览器加载的静态文件,以及封装运行环境的镜像。它们依次产生,也可能分别发布。先识别项目的构建入口与输出,再决定目标机器需要哪些文件和运行时。
源码 + 工具链 + 依赖/插件 + 构建配置
↓ 编译、测试、打包
├── Boot JAR → Java 运行时
├── 静态目录 → Web 服务器
├── SSR 服务 → Node 运行时与运行依赖
└── 镜像 → 容器运行时构建机可以有编译器、测试库和仓库凭据。运行机只接收经过验证的产物与必需的运行依赖,数据库密码等运行配置另行提供。这样能减少运行机的软件和权限,也能把一次发布准确对应到某个产物。
从一个 Java 工程得到可执行 JAR
源码和工具链
下载完整部署示例工程,解压进入 deploy-app。它使用 Java 25、Maven 3.9.12 和 Spring Boot 4.1.1,提供 /api/greeting 接口、健康探针及四个测试。构建与运行版本应由项目约定决定;升级时检查 Spring Boot 系统要求。
deploy-app/
├── pom.xml
├── src/main/java/dev/example/deploy/DeployApplication.java
├── src/main/resources/application.properties
├── src/test/java/dev/example/deploy/DeployApplicationTest.java
├── Dockerfile、.dockerignore、compose.yaml
└── deploy-app.servicepom.xml 以 spring-boot-starter-parent:4.1.1 为 parent,设置 java.version=25,依赖 Web MVC、Actuator 和测试 starter。构建部分如下:
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution><goals><goal>build-info</goal></goals></execution>
</executions>
</plugin>
</plugins>
</build>parent 提供 repackage 执行约定,插件把应用类与运行依赖装入可执行 JAR。没有这个 parent 的工程,通常需要自己绑定 repackage;仅在插件列表写一个名字不能保证普通 JAR 变成 Boot JAR。build-info 另生成版本信息供接口读取。Boot Maven 打包说明
Linux 主机已安装匹配 JDK 与 Maven 时,在工程根目录执行:
java -version
mvn -version
mvn -B -ntp clean verifymvn -version 同时显示 Maven 实际使用的 Java Home。它可能与另一个终端里的 java 不同。出现 release version 25 not supported 时先修正构建 JDK,不要直接降低项目语言版本绕过编译错误。
不安装宿主 JDK 时,可以用 Docker 构建。下面在 Linux 的工程目录执行,宿主操作者具有 Engine 权限,构建容器显式使用宿主 UID/GID:
mkdir -p .build-home .m2
docker run --rm --user "$(id -u):$(id -g)" \
-e MAVEN_CONFIG=/work/.m2 -e HOME=/work/.build-home \
--mount "type=bind,src=$PWD,dst=/work" -w /work \
maven:3.9.12-eclipse-temurin-25 \
mvn -B -ntp -Duser.home=/work/.build-home \
-Dmaven.repo.local=/work/.m2 clean verify当前账号必须能写工程、输出和缓存目录。如果此前用 root 容器生成了这些目录,应由管理员只修复该工程相应目录的所有权;不要对整块磁盘递归修改。镜像拉取由 Engine 完成,Maven 下载依赖发生在构建容器内,两者使用的代理和 CA 配置分别处理。
测试结果与打包结果
成功时可见 Tests run: 4, Failures: 0, Errors: 0 和 BUILD SUCCESS,输出在 target/。测试分别检查配置覆盖后的业务返回、readiness、未暴露管理接口的 404,以及慢请求参数超限的 400。四个测试验证的是这些行为,不能代替项目未编写的业务测试。
validate → compile → test → package → verify → install → deploy
│ │ │
单元测试 形成产物 执行已绑定的校验这是 Maven 默认生命周期的一部分。调用 verify 会经过前面的阶段,但集成测试是否执行取决于 Failsafe 等插件的绑定;没有绑定就不会自动出现一套集成测试。install 写本地 Maven 仓库,deploy 发布到远程制品仓库,它们都不是把应用部署成服务器进程。Maven 生命周期
测试失败先查看 target/surefire-reports/;集成测试使用 Failsafe 时再查 target/failsafe-reports/。-DskipTests 通常跳过测试执行但仍编译测试,-Dmaven.test.skip=true 还影响测试编译。实际语义由对应插件决定,临时跳过后得到的 JAR 不应标记成已完成同样验证。Surefire 跳过测试选项
检查拿到的文件
在具有 JDK、unzip 和 GNU coreutils 的 Linux 环境执行:
test -s target/app.jar
jar tf target/app.jar | sed -n '1,16p'
unzip -p target/app.jar META-INF/MANIFEST.MFBoot JAR 的应用类在 BOOT-INF/classes/,依赖在 BOOT-INF/lib/,manifest 的 Main-Class 指向 Boot loader,Start-Class 指向 dev.example.deploy.DeployApplication。本工程的构建信息也随应用资源放在 classes 下:
unzip -p target/app.jar BOOT-INF/classes/META-INF/build-info.properties普通 JAR 可能只有应用类,并依赖外部 classpath;Boot 打包留下的 app.jar.original 也是重打包前的文件。不要用 target/*.jar* 选择运行产物。WAR 则要区分外部 Servlet 容器部署与带可执行启动器的 WAR,按项目实际打包配置确定。
java -jar app.jar --version 会执行应用 main;只有应用实现了该参数,它才是版本查询。查看 manifest 和构建属性不会启动服务,首次启动验证放在隔离端口上做。
依赖、仓库与其他构建入口
Maven 解析了什么
一个依赖至少由 groupId、artifactId 和 version 确定,type、classifier 还会区分不同产物。直接依赖会带入传递依赖;parent、BOM 和 dependencyManagement 可以管理版本,但管理版本本身不等于引入依赖。插件也需要下载,因此“库都缓存了”仍可能缺构建插件。
mvn -B -ntp dependency:tree
mvn -B -ntp help:effective-pom -Doutput=effective-pom.xml先用依赖树判断实际选择的版本,再看有效 POM 中继承、profile 和插件绑定。诊断输出可能含内部仓库地址,应保留在受控目录。依赖冲突不能只看源码里直接声明的那一行。依赖机制
企业 Maven 仓库一般同时代理中央仓库和托管内部产物。settings.xml 中 mirror 负责把仓库请求映射到目标地址,server 凭据的 id 必须匹配最终访问的仓库或 mirror id。例如 mirror id 为 corp-maven,对应 server 也应使用这个 id。Maven settings
凭据由运行构建的账号通过受保护配置提供,不提交到 POM、镜像层或日志。只读依赖下载与制品发布使用不同权限。出现 401/403 查身份、id 和仓库权限;PKIX 错误查构建 JDK 的信任库,不能用跳过证书验证解决。
多模块从仓库根目录构建。只构建某个服务及其依赖可用 mvn -pl service-module -am verify,模块名需替换成实际路径或选择器。失败后 -rf 仅恢复后续 reactor 顺序,前面的依赖若没有可用产物,仍可能解析失败;这时重新运行所需 reactor 比反复清空全局缓存更明确。
Gradle 项目的对应关系
项目带 gradlew、gradlew.bat 和 gradle/wrapper/ 时,优先使用项目 Wrapper。gradle-wrapper.properties 固定 Gradle distribution,distributionSha256Sum 可校验下载包;Wrapper 脚本和 JAR 自身也应纳入代码审查。Gradle Wrapper
在可信 Gradle 工程根目录执行:
./gradlew --version
./gradlew tasks --all
./gradlew clean buildbuild 聚合哪些任务由插件和工程定义决定。Java 项目常经 check 运行测试,再经 assemble 打包;额外集成测试任务必须进入依赖图。多项目可用 ./gradlew :service:build 精确选择实际子项目。
Spring Boot 工程检查 bootJar 的归档文件名;普通 jar 和 Boot JAR 可以同时存在。用项目任务的 archiveFile 配置确定输出,而不是把 build/libs/ 所有 JAR 都交给运行机。启动 daemon 是构建加速机制,CI 的 --no-daemon 选择也不能弥补未声明输入或错误任务缓存。Gradle 构建生命周期
Node 项目:脚本决定构建动作
Node 版本、包管理器和 lockfile 要配套。已有 package-lock.json 的 npm 工程使用 npm ci;它要求 lockfile 与 package.json 一致,并重建 node_modules。构建常需要 devDependencies,不能在编译前就省略开发依赖。npm ci
node --version
npm --version
npm ci
npm test
npm run build这组命令适用于项目确实声明了 test 和 build 脚本的情况。脚本可以启动任意程序,应先检查可信项目的 package.json;没有测试脚本时,不能把 npm test 的失败改成静默忽略,再声称测试通过。
pnpm 工程使用已锁定的 pnpm 版本和 pnpm-lock.yaml:
pnpm --version
pnpm install --frozen-lockfile
pnpm test
pnpm buildfrozen 模式禁止安装过程偷偷更新锁文件。依赖安装还可能执行生命周期脚本,禁用脚本会影响原生模块和部分工具;应按包管理器版本审查允许执行的包,而非在所有项目套一个开关。pnpm install
跨系统搬运 node_modules 容易遇到原生扩展、ABI、libc 和 CPU 架构差异。构建缓存应至少区分操作系统、架构、Node 与包管理器版本、锁文件和相关构建配置。前端静态产物通常可跨平台部署,SSR 的运行依赖却可能仍有平台要求。
浏览器代码中的接口地址经常在构建时替换;修改运行容器环境变量不会自动重写已经生成的 JS。所有进入浏览器包的值都可能被用户读取,禁止把数据库密码、私钥和服务端令牌作为前端构建变量。前端部署说明静态配置和资源缓存的配合。
从构建目录交付到运行环境
明确产物与运行要求
| 产物 | 交付内容 | 运行环境检查 |
|---|---|---|
| Boot JAR | 精确 JAR、摘要、版本记录 | Java 版本、配置路径、监听地址与身份 |
| 普通 JAR | JAR 与约定依赖/classpath | 主类、依赖版本与加载路径 |
| 静态站点 | 构建输出目录的完整内容 | base path、MIME、前端路由与缓存 |
| SSR 应用 | 框架指定服务器输出与运行依赖 | Node、入口文件、静态目录、原生模块 |
| 容器镜像 | 目标平台镜像及不可变引用 | CPU 架构、镜像来源、运行参数与持久化 |
dist、.next 或 build 是目录名,不能单靠名字判断部署方式。例如 SSR 框架的内部构建目录未必能直接作为独立服务器包使用;先看项目选择的是静态导出、普通 server 还是 standalone 输出。
构建成功后只封装本次需要的文件。Linux 下在 Java 示例根目录执行,目标目录使用本次专用的 delivery-1,若已存在则停下来检查,不覆盖旧交付:
mkdir delivery-1
install -m 0644 target/app.jar delivery-1/app.jar
(
cd delivery-1
sha256sum app.jar > SHA256SUMS
sha256sum -c SHA256SUMS
)应得到 app.jar: OK。摘要文件使用相对于交付目录的文件名,因此接收方在相同目录即可复核,不依赖构建机的 target 路径。
摘要验证传输完整性;若 JAR 与摘要都被替换,单独比对它们无法认证发布者。正式交付还应由受控仓库、签名或发布系统提供可信来源,并保存源码提交、工具链、测试结果和产物之间的对应关系。
下面的负例只修改交付副本,原始 target/app.jar 保留:
printf 'modified' >> delivery-1/app.jar
(cd delivery-1 && sha256sum -c SHA256SUMS)预期非零退出并提示 FAILED。不要重算摘要把损坏合理化;重新从已验证的 target 文件复制,再检查原摘要:
install -m 0644 target/app.jar delivery-1/app.jar
(cd delivery-1 && sha256sum -c SHA256SUMS)运行检查与发布记录
本机已有 Java 25 时,在一个终端启动交付文件,用显式高端口避开默认服务:
java -jar delivery-1/app.jar --server.port=18091 --app.message=delivery-check另一个终端执行:
curl -q --noproxy '*' --fail-with-body --silent --show-error --max-time 5 \
http://127.0.0.1:18091/api/greeting应返回 message 为 delivery-check、version 为 1.0.0 的 JSON。完成后在启动终端按 Ctrl+C。启动失败依次看 Java 字节码兼容、缺失依赖、配置加载和监听端口,不能只靠文件存在认定可运行。
发布记录把代码版本与运行配置分开:同一个 JAR 可以在不同数据库账号、回调地址或流量规则下运行;回滚 JAR 不会自动恢复这些配置。应用服务的目录布局、systemd、容器参数和更新恢复见 Spring Boot 部署运维。
构建时注入的时间、随机值、未固定依赖和平台差异,都可能让相同源码产生不同字节。需要字节级可复现时,固定完整输入并处理归档时间等非确定因素,再比较两次独立构建结果;不能把“同一个 Git 提交”直接当作摘要相同的保证。Maven 可复现构建
缓存与离线环境
中国大陆网络或企业内网不能访问公共仓库时,先使用组织批准的代理、制品私服与 CA。不要随意把所有仓库替换成来源不明的镜像站;下载权限和发布权限要分开,代理不能破坏校验与来源追踪。
离线准备要运行最终需要的完整目标。对于前面的容器构建,联网成功后再复跑:
docker run --rm --network none --user "$(id -u):$(id -g)" \
-e MAVEN_CONFIG=/work/.m2 -e HOME=/work/.build-home \
--mount "type=bind,src=$PWD,dst=/work" -w /work \
maven:3.9.12-eclipse-temurin-25 \
mvn -o -B -ntp -Duser.home=/work/.build-home \
-Dmaven.repo.local=/work/.m2 clean verify镜像需要已存在于本机。-o 让 Maven 按离线模式解析,--network none 同时暴露测试、插件或脚本自行联网的情况。此结果只适用于相同项目输入、平台、profile 和构建目标;换一个目标仍可能缺资源。
dependency:go-offline 可以辅助预取,却未必覆盖后续插件动态解析或测试下载。Gradle 的 --offline、npm 的缓存、pnpm store 也要用最终命令做断网复跑。离线运行则只需要已构建制品与运行依赖,两者不要混成一个巨大的开发机缓存包。离线部署进一步说明镜像包、系统依赖与恢复介质。
构建失败的定位顺序
先保留首个失败模块、任务、异常及其上下文,再查看末尾汇总。重跑时只改变一个相关条件,才能判断修复是否有效。
| 现象 | 判断与下一步 |
|---|---|
| 401/403、artifact not found | 区分认证拒绝与坐标不存在;核对最终仓库 id、权限、版本和已发布内容 |
| PKIX、certificate verify failed | 查看实际访问主机与工具使用的 CA;修正构建 JDK/Node 信任配置,保持 TLS 校验 |
| timeout、DNS 失败 | 分别检查 Engine 拉取与构建容器网络;对照批准的代理、DNS 和仓库连通性 |
| 编译器不支持 release | 看 mvn -version 或 Gradle toolchain 实际 JDK,按项目要求切换 |
| 测试失败 | 从测试报告定位断言或异常;有外部依赖时检查测试数据、端口和初始化,修复后跑同一目标 |
| Node lockfile 不一致 | 在开发分支按规定包管理器更新并审查锁文件;CI 不自动改锁继续交付 |
| native module 加载失败 | 比较 Node ABI、平台、架构和 libc;在匹配环境重新安装/构建 |
| BUILD SUCCESS 但启动失败 | 对照精确产物、manifest、运行 JDK 与外部配置,再做隔离启动 |
| 磁盘不足 | 查看工程输出、缓存和容器占用;只清理已确认可重建且无并发使用的目标 |
修复仓库故障后可对相关 Maven 构建使用 -U 重新检查缺失 release 与 snapshot;它不是解决权限和错误坐标的开关。删除整个 .m2 往往把单个失败变成全量重下载,内网环境还可能因此失去唯一可用缓存。
构建机执行依赖脚本和插件,权限接近运行它的账号。未审查的外部分支不应共享生产发布凭据;工作目录、缓存与发布目标按信任程度隔离。构建通过之后再授予对应发布步骤最小权限,能缩小依赖或脚本被篡改时的影响。
权威资料与规范地址
Java 与 Maven 构建
Gradle 与前端依赖
| 查阅内容 | 官方地址 |
|---|---|
| Gradle Wrapper | https://docs.gradle.org/current/userguide/gradle_wrapper.html |
| Gradle 构建生命周期 | https://docs.gradle.org/current/userguide/build_lifecycle.html |
| npm ci | https://docs.npmjs.com/cli/v10/commands/npm-ci/ |
| pnpm install | https://pnpm.io/cli/install |
