适用读者:遇到"本地能跑、CI 挂""依赖冲突找不到类""打出的包多了一个版本"的人。
前置:S06-B01(Maven 三层模型与多模块构建)、A02(构建可复现与制品治理)、J01(多模块工程结构)。
覆盖旧文:2891(Maven 构建 Java 项目)、2892(仓库)、2893(依赖管理)、2894(引入外部依赖)、2895(插件)、2896(教程总览)、2897(Maven 构建 Java 项目)、2898(构建与测试)、2899(生命周期)、2900(Profiles)、2901(环境配置)、2902(Archetype)→ 12 篇 / 约 4.5 万字符。
扩展点:E-S06-018(生命周期与插件绑定的因果链:mvn一条命令到底跑了什么)、E-S06-019(依赖冲突三种表现与dependencyManagement/enforcer兜底)、E-S06-020(Profiles 的滥用与"环境隔离的正确落点")。
上一篇:S06-B06 · Java 生态四库实测:JJWT / sensitive-word / tianai-captcha / Commons Lang3
下一篇:S06 系列验收(v2)。
1. 一句话结论
Maven 的行为 = 生命周期阶段 + 绑定的插件执行。看不懂 mvn 跑了什么,就调不动构建;"本地能跑 CI 挂"九成是三处差异:JDK 版本、Profile 激活、依赖解析顺序。
出处:
A-2899(生命周期)、A-2895(插件)、A-2900(Profiles)、A-2893(依赖管理)。
2. 生命周期与插件绑定(E-S06-018)
三套生命周期:
clean : pre-clean → clean → post-clean
default: validate → compile → test → package → verify → install → deploy
site : pre-site → site → post-site → site-deploy
一条命令执行到某阶段,之前的阶段全部依次执行:
mvn package = validate…compile → test → package(不会执行 verify/install)
默认绑定(jar 打包):
| 阶段 | 默认插件目标 |
|---|---|
process-resources |
maven-resources-plugin:resources |
compile |
maven-compiler-plugin:compile |
test |
maven-surefire-plugin:test |
package |
maven-jar-plugin:jar |
install |
maven-install-plugin:install |
自定义绑定(把插件挂到阶段上):
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>print-version</id>
<phase>validate</phase> <!-- 挂到哪个阶段 -->
<goals><goal>run</goal></goals>
<configuration><target><echo message="${project.version}"/></target></configuration>
</execution>
</executions>
</plugin>
判据:"为什么我的插件没跑"= 没绑到阶段,或用了 mvn 未到达该阶段。用 mvn -X 或 mvn help:describe 定位。
正确姿势(可复现构建):
mvn -B -q -DskipTests package # CI:批处理模式、安静、跳测试
mvn -B verify -Pit # 集成测试 profile
mvn dependency:tree -Dincludes=com.foo:bar # 查某个依赖从哪来
mvn -X compile 2>&1 | grep -A3 'Goal' # 看实际执行的插件目标
3. 依赖冲突的三种表现与兜底(E-S06-019)
| 表现 | 症状 | 定位 |
|---|---|---|
| 版本仲裁 | 运行时用的是"较近/先声明"的旧版本 | mvn dependency:tree -Dverbose 看 omitted for conflict |
| 传递依赖缺失 | NoClassDefFoundError(编译过、运行挂) |
mvn dependency:tree 对比声明 |
| 包冲突 | NoSuchMethodError、LinkageError |
常见于 javax vs jakarta、log4j vs log4j2 |
三类修法与优先级:
① dependencyManagement(推荐):在父 POM 统一版本,子模块不写版本
② exclusions:排除不需要的传递依赖(谨慎,可能连带丢必需类)
③ 直接声明版本(最暴力):覆盖仲裁结果,但会掩盖真实冲突
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version> <!-- 一处定版,全工程生效 -->
</dependency>
</dependencies>
</dependencyManagement>
兜底(防止别人以后又引歪):
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<executions><execution>
<id>enforce</id><goals><goal>enforce</goal></goals>
<configuration><rules>
<dependencyConvergence/> <!-- 冲突即失败 -->
<requireJavaVersion><version>[17,)</version></requireJavaVersion>
<banDuplicatePomDependencyVersions/>
</rules></configuration>
</execution></executions>
</plugin>
判据:dependencyConvergence 让冲突在构建期暴露,而不是上线后 NoSuchMethodError。引入它会暴露一批历史问题——这是好事,分批修。
4. Profiles:正确落点与三种滥用(E-S06-020)
Profile 能做:改属性、改依赖、改插件配置、改 <repositories>。
Profile 不该做:把"环境差异"塞进 POM(生产者的配置不该由构建产物决定)。
<profiles>
<profile>
<id>it</id>
<activation><activeByDefault>false</activeByDefault></activation>
<build><plugins>
<plugin><artifactId>maven-failsafe-plugin</artifactId>
<executions><execution><goals><goal>integration-test</goal><goal>verify</goal></goals></execution></executions>
</plugin>
</plugins></build>
</profile>
</profiles>
| 滥用 | 后果 | 正确落点 |
|---|---|---|
dev/prod 两个 profile 打包不同 jar |
制品不可复现(同一 commit 产两种包) | 一份制品 + 外部配置(env/ConfigMap/启动参数) |
| profile 里写数据库密码 | 密码进仓库与制品 | 环境变量/密钥管理 |
靠 activeByDefault 控制行为 |
命令行 -P 会覆盖,行为不一致 |
显式 -P<id>,CI 固定写出 |
判据:"构建期决定环境"是可复现性最大的敌人——同一 commit 必须产出同一制品;环境差异只在启动配置层。
5. 仓库与私服(2892/2901)
本地仓库 ~/.m2/repository → 私服(Nexus/Artifactory)→ 中央仓库
settings.xml 里配 mirror + server 凭据;不要在 POM 里写 <repositories> 指向内网地址
| 问题 | 原因 | 处置 |
|---|---|---|
| CI 拉不到依赖 | 私服地址/凭据只在某人 settings.xml |
用 CI 的 settings 模板 + 凭据注入 |
| 每次构建都下载 | 无本地缓存或 -U 滥用 |
CI 缓存 ~/.m2;只在需要更新 SNAPSHOT 时 -U |
| 制品被覆盖 | 用了 SNAPSHOT 做发布 | 发布只用 release 版本 + 不可变仓库策略 |
| 构建不可复现 | 依赖了 LATEST/RELEASE/范围版本 |
全部固定版本号 |
Archetype(2902)的现代替代:脚手架更适合做成"公司模板仓库 + 参数化脚本",而非维护 archetype-catalog(创建慢、升级难、IDE 兼容差)。
6. 反例:七种"构建不稳定"
| 现象 | 根因 |
|---|---|
| 本地成功、CI 失败 | JDK 版本不同(source/target 与运行 JRE 不一致) |
| 同一 commit 两个包 | 靠 profile 打包不同环境配置 |
NoSuchMethodError |
传递依赖仲裁到旧版本 |
| 构建时长忽长忽短 | 未缓存 ~/.m2,或每次都 -U |
| 别人机器编不过 | 依赖了本地 install 的 SNAPSHOT |
| 打包多出测试类 | maven-jar-plugin 未排除,或 test-jar 误引 |
| 时间戳导致不可复现 | 未开 project.build.outputTimestamp |
7. 自测
mvn package实际执行哪些阶段?为什么install不会跑?- 插件"没跑"的两个原因?
- 依赖冲突三种表现与三类修法?
dependencyConvergence解决什么?- 为什么"用 profile 打包不同环境"是错误的?
答案
- validate→…→test→package;生命周期按阶段顺序执行,未指定 install 就不执行。
- 未绑定到阶段;命令未到达该阶段。
- 版本仲裁/传递缺失/包冲突;dependencyManagement、exclusions、直接声明。
- 构建期暴露版本冲突,避免运行期 NoSuchMethodError。
- 同一 commit 产出不同制品,破坏可复现性;环境差异应在启动配置层。
8. 下一篇
S06 系列验收(v2) —— 13 篇 / 1,901 点位 / 16 篇尾部旧文 100% 登记 / 下一系列(P16:S15 14 篇)前置依赖。
本篇属于系列《S06 · Java 工程化与组件》。旧文来源:A-2891–2902(Maven 教程 12 篇);扩展点 E-S06-018/025/026。库版本口径为 2026-11 时点,具体小版本 API 以官方文档为准(待核)。