目标:掌握依赖范围、传递依赖、冲突解决、BOM、生命周期、Profile 与资源过滤,能解释并治理真实项目构建行为。
目录
- [依赖范围 Scope](#依赖范围 Scope)
- 传递依赖与冲突规则
- [dependencyManagement 与 BOM](#dependencyManagement 与 BOM)
- 构建生命周期
- [Profile 机制](#Profile 机制)
- 资源过滤
- [实战 Demo:maven-demo-core](#实战 Demo:maven-demo-core)
- 常见问题与避坑
- 专家面试题
1. 依赖范围 Scope
Maven 的 scope 决定依赖在编译、测试、运行、打包时是否可见。
| Scope | 编译 | 测试 | 运行 | 是否传递 | 典型依赖 |
|---|---|---|---|---|---|
compile |
是 | 是 | 是 | 是 | 业务库、工具库 |
provided |
是 | 是 | 否 | 否 | Servlet API、Lombok |
runtime |
否 | 是 | 是 | 是 | JDBC 驱动 |
test |
否 | 是 | 否 | 否 | JUnit、Mockito |
system |
是 | 是 | 否 | 否 | 本地 jar,不推荐 |
import |
仅用于 dependencyManagement |
- | - | - | BOM |
示例:
xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
test 依赖不会进入主代码运行时 classpath,因此不能在 src/main/java 中引用 JUnit 类。
2. 传递依赖与冲突规则
传递依赖
如果 A 依赖 B,B 依赖 C,那么 A 默认也会获得 C,这就是传递依赖。
text
maven-demo-web
└── maven-demo-core
└── maven-demo-api
web 直接依赖 core,而 core 依赖 api。因此 web 可以在运行时获得 api。
冲突解决规则
Maven 依赖冲突主要按两条规则处理:
| 规则 | 含义 |
|---|---|
| 最短路径优先 | 离当前项目最近的依赖版本胜出 |
| 路径相同先声明优先 | 依赖路径长度相同时,先声明的依赖胜出 |
排查命令:
bash
mvn dependency:tree
mvn dependency:tree -Dincludes=org.apache.commons:commons-lang3
mvn dependency:tree -Dverbose
排除传递依赖
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-sdk</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
排除依赖前必须确认:
- 当前运行路径确实不需要它。
- 有替代实现或更高版本。
- 测试覆盖相关功能。
3. dependencyManagement 与 BOM
dependencyManagement 只管理版本,不会自动引入依赖。
父 POM 中:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>
子模块中:
xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
子模块不用写版本,版本从父 POM 继承。
BOM 模式
BOM 是 Bill of Materials,通常是 packaging=pom,专门管理一组依赖版本。
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
本 Demo 同时提供 maven-demo-bom/,用于演示公司内部组件如何输出 BOM。
BOM 的企业价值
| 场景 | BOM 解决方式 |
|---|---|
| 多个微服务依赖版本不一致 | 统一导入公司 BOM |
| Spring Boot、Spring Cloud 版本兼容复杂 | 使用官方 BOM |
| SDK 对外发布 | 提供 BOM,调用方无需逐个写版本 |
| 漏洞修复 | BOM 升级一次,全项目统一收敛 |
4. 构建生命周期
Maven 有三套内置生命周期:
| 生命周期 | 作用 | 常见阶段 |
|---|---|---|
clean |
清理构建产物 | pre-clean、clean、post-clean |
default |
主构建流程 | validate、compile、test、package、verify、install、deploy |
site |
生成项目站点 | site、site-deploy |
default 生命周期关键阶段
text
validate
↓
compile
↓
test
↓
package
↓
verify
↓
install
↓
deploy
执行 mvn package 会从 validate 一直执行到 package。执行 mvn test 不会执行 package。
Goal 与 Phase
Phase 是生命周期阶段,Goal 是插件目标。
text
phase: compile
goal : maven-compiler-plugin:compile
一个 Phase 可以绑定多个 Goal,一个 Goal 也可以手动执行:
bash
mvn compiler:compile
mvn dependency:tree
5. Profile 机制
Profile 用于在不同环境下切换配置。
本 Demo 父 POM:
xml
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<demo.environment>local</demo.environment>
<demo.log.level>DEBUG</demo.log.level>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<demo.environment>prod</demo.environment>
<demo.log.level>WARN</demo.log.level>
</properties>
</profile>
</profiles>
激活方式:
bash
mvn package -Pprod
mvn help:active-profiles
mvn help:effective-pom -Pprod
Profile 适合切换构建参数,不建议把大量业务配置写进 POM。Spring Boot 应用的运行配置仍应优先使用 application-dev.yml、环境变量或配置中心。
6. 资源过滤
资源过滤可以把 Maven 属性写入资源文件。
maven-demo-core/pom.xml:
xml
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
build-info.properties:
properties
artifactId=${project.artifactId}
version=${project.version}
environment=${demo.environment}
logLevel=${demo.log.level}
构建后 target/classes/build-info.properties 会变成:
properties
artifactId=maven-demo-core
version=1.0.0-SNAPSHOT
environment=local
logLevel=DEBUG
验证:
bash
cd maven-demo
mvn -pl maven-demo-core clean package
cat maven-demo-core/target/classes/build-info.properties
7. 实战 Demo:maven-demo-core
目标
演示:
- 父 POM 统一依赖版本。
maven-demo-core不写依赖版本。- Profile 注入环境信息。
- 资源过滤生成构建元数据。
关键代码
GreetingService:
java
public GreetingResponse greet(GreetingRequest request) {
String name = StringUtils.capitalize(request.normalizedName());
return new GreetingResponse(
"Hello, " + name,
buildInfo.environment(),
buildInfo.version()
);
}
运行:
bash
cd maven-demo
mvn -pl maven-demo-core test
mvn -pl maven-demo-core package -Pprod
cat maven-demo-core/target/classes/build-info.properties
预期输出:
properties
environment=prod
logLevel=WARN
8. 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 子模块依赖没写版本却能构建 | 父 POM dependencyManagement 管理了版本 |
用 mvn help:effective-pom 查看 |
dependencyManagement 写了依赖但没生效 |
它只管版本,不自动引入依赖 | 子模块仍要写 <dependency> |
| Profile 没切换 | 未使用 -P 或被默认 Profile 覆盖 |
mvn help:active-profiles |
| 资源过滤后中文乱码 | 编码未统一 | 设置 project.build.sourceEncoding=UTF-8 |
system scope 依赖丢失 |
system 依赖不传递且不推荐 | 发布到私服或本地仓库 |
9. 专家面试题
Q1:dependencyManagement 和 dependencies 的区别是什么?
答:dependencies 会真实引入依赖;dependencyManagement 只提供版本、scope、exclusion 等默认值,不会自动加入 classpath。子模块声明同一依赖但不写版本时,才会继承管理信息。企业项目通常在父 POM 或 BOM 中做 dependencyManagement,在具体模块中按需声明 dependencies。
Q2:Maven 如何解决同一个依赖多个版本冲突?
答:Maven 使用 nearest definition 策略,即依赖路径最短的版本优先。如果路径长度相同,先声明的依赖优先。这个机制可预测但不一定符合业务期望,因此企业项目要用 dependencyManagement 主动锁定关键依赖版本,并用 mvn dependency:tree 做验证。
Q3:为什么 BOM 的 scope 是 import?
答:import scope 只能出现在 dependencyManagement 中,用于把另一个 POM 的依赖管理清单导入当前项目。它不会把 BOM 作为普通 jar 加入 classpath,因为 BOM 的价值是版本清单,不是运行时代码。
Q4:Profile 适合解决什么问题,不适合解决什么问题?
答:Profile 适合切换构建参数、资源过滤、插件执行、发布仓库等构建期差异。不适合承载大量运行期业务配置,因为 POM 变更需要重新构建,生产配置也不应写死在代码仓库中。
10. 进阶篇扩展核查:依赖治理深水区
10.1 optional 依赖
optional=true 表示该依赖对当前模块编译可见,但不会传递给下游项目。
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>feature-extension</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
典型场景:
- 一个 SDK 支持 Redis、Kafka、JDBC 多种扩展,但调用方只使用其中一种。
- 框架模块希望编译期适配某个库,但不强迫所有使用方引入它。
- 减少传递依赖污染,降低冲突概率。
判断标准:如果下游不一定需要这个依赖,就考虑 optional;如果下游运行一定需要,就不要 optional。
10.2 classifier 与 type
同一个 GAV 下可能有多个附属产物,classifier 用来区分这些产物。
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-sdk</artifactId>
<version>1.0.0</version>
<classifier>tests</classifier>
<type>test-jar</type>
</dependency>
常见组合:
| 用途 | classifier | type |
|---|---|---|
| 源码包 | sources |
java-source |
| Javadoc | javadoc |
javadoc |
| 测试工具包 | tests |
test-jar |
| 原生平台包 | linux-x86_64 |
jar |
企业发布 SDK 时,建议同时发布主 JAR、源码包和 Javadoc,便于调用方调试和审计。
10.3 exclusions 的边界
排除依赖不是越多越好。错误排除会导致运行期 ClassNotFoundException 或行为缺失。
排除前要做三步:
bash
mvn dependency:tree -Dincludes=groupId:artifactId
mvn test
mvn -pl affected-module -am verify
推荐记录排除原因:
xml
<exclusion>
<!-- 使用 spring-jcl 替代 commons-logging,避免日志桥接冲突 -->
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
10.4 依赖收敛
依赖收敛指同一个依赖在整棵依赖树中只出现一个版本。大型项目最容易出现 Jackson、Netty、Guava、SLF4J、Micrometer 等依赖版本漂移。
可以使用 Enforcer:
xml
<requireUpperBoundDeps/>
<dependencyConvergence/>
但注意:严格收敛在复杂 Spring Boot 项目中可能产生大量误报,需要结合 BOM 和排除策略逐步治理。
10.5 运行时 classpath 与编译时 classpath
compile 成功不代表运行成功。原因是编译 classpath 和运行 classpath 不完全一样。
例子:
| Scope | 编译可见 | 运行可见 |
|---|---|---|
| compile | 是 | 是 |
| provided | 是 | 否 |
| runtime | 否 | 是 |
| test | 测试可见 | 主运行不可见 |
如果你把 JDBC 驱动写成 provided,编译可能成功,但运行连接数据库时会找不到 Driver。
10.6 Profile 激活方式详解
Profile 可以通过多种方式激活:
| 方式 | 示例 | 适用场景 |
|---|---|---|
| 命令行 | mvn package -Pprod |
手动切环境 |
| 默认激活 | <activeByDefault>true</activeByDefault> |
本地开发默认值 |
| JDK 激活 | <jdk>[17,)</jdk> |
JDK 版本差异 |
| OS 激活 | <os><family>mac</family></os> |
操作系统差异 |
| 属性激活 | -Denv=prod |
CI 参数驱动 |
| 文件激活 | 文件存在时激活 | 本地私有配置 |
排查命令:
bash
mvn help:active-profiles
mvn help:all-profiles
一个容易踩的坑:只要显式激活了同一个 POM 中的其他 Profile,activeByDefault Profile 就可能不再激活。因此关键默认值不要只放在 activeByDefault Profile 中,建议在基础 properties 中提供默认值,再由 dev/test/prod 覆盖。Demo 的 demo.environment=local 和 demo.log.level=DEBUG 就采用这种方式,避免执行 mvn -Pquality verify 时资源过滤变量丢失。
10.7 资源过滤风险
资源过滤很有用,但也有风险。
| 风险 | 示例 | 解决方案 |
|---|---|---|
| 二进制文件被过滤损坏 | 图片、证书、字体 | 不要对二进制目录开启 filtering |
| 占位符误替换 | ${...} 被 Maven 当变量 |
使用不同 delimiter 或关闭过滤 |
| 密钥进入产物 | ${password} 被写入配置 |
密钥走环境变量或配置中心 |
| 多环境配置混乱 | POM Profile 和 Spring Profile 混用 | 明确构建期和运行期边界 |
10.8 生命周期阶段补全
default 生命周期远不止常见的 7 个阶段。
| 阶段 | 作用 |
|---|---|
validate |
校验项目 |
initialize |
初始化构建状态 |
generate-sources |
生成源码 |
process-sources |
处理源码 |
generate-resources |
生成资源 |
process-resources |
复制和过滤资源 |
compile |
编译主代码 |
process-test-resources |
复制测试资源 |
test-compile |
编译测试代码 |
test |
执行单元测试 |
prepare-package |
打包前准备 |
package |
生成产物 |
pre-integration-test |
集成测试前准备 |
integration-test |
集成测试 |
post-integration-test |
集成测试后清理 |
verify |
校验结果 |
install |
安装到本地仓库 |
deploy |
发布到远程仓库 |
10.9 跳过测试的区别
| 参数 | 编译测试代码 | 执行测试 | 风险 |
|---|---|---|---|
-DskipTests |
是 | 否 | 能发现测试代码编译错误 |
-Dmaven.test.skip=true |
否 | 否 | 可能掩盖测试代码已损坏 |
推荐:
- 本地临时打包用
-DskipTests。 - CI 主线不要跳测试。
- 发布流水线至少执行
verify。
10.10 进阶实操任务
| 任务 | 命令 | 目标 |
|---|---|---|
| 查看 Web 依赖树 | mvn -pl maven-demo-web -am dependency:tree |
理解传递依赖 |
| 切换生产 Profile | mvn -pl maven-demo-core package -Pprod |
验证资源过滤 |
| 查看 Effective POM | mvn help:effective-pom -Pprod |
验证 Profile 合并 |
| 只构建 CLI 和上游 | mvn -pl maven-demo-cli -am package |
验证 Reactor |
| 分析依赖使用 | mvn dependency:analyze |
识别未声明/未使用依赖 |
10.11 进阶篇新增面试题
Q5:optional 和 exclusion 的区别是什么?
答:optional 是依赖提供方声明"这个依赖不要自动传递给下游";exclusion 是依赖使用方声明"我不接受某个传递依赖"。前者由上游控制,后者由下游控制。optional 适合框架扩展能力,exclusion 适合解决冲突或替换实现。
Q6:为什么 mvn verify 比 mvn package 更适合 CI?
答:package 只保证产物生成,verify 会执行到更靠后的验证阶段,适合绑定集成测试、质量检查、覆盖率门禁和安全扫描。CI 的目标不是只打包,而是证明代码满足质量标准。
Q7:BOM 和 Parent POM 是否可以互相替代?
答:不能完全替代。Parent POM 通过继承管理插件、属性、Profile 和构建规则;BOM 通过 import 管理依赖版本。多仓库项目可能不能共享同一个 Parent,但仍可统一导入公司 BOM。