三坐标与依赖树
Maven 用 groupId、artifactId、version 三个坐标定位一个 jar。本地仓库默认在 ~/.m2/repository,公司一般会再配一个私服。项目里直接写的依赖只是一部分,更多 jar 是通过“依赖传递”间接进来的:A 依赖 B,B 依赖 C,那么 A 的 classpath 上就有 C。
依赖一多,版本冲突就来了。mvn dependency:tree 打印出来的就是完整的依赖树,排冲突基本都从它开始看。
scope 有哪几种
| scope | 编译期 | 运行期 | 是否传递 | 典型用途 |
|---|---|---|---|---|
| compile | 有 | 有 | 是 | 默认值,绝大多数依赖 |
| provided | 有 | 无 | 否 | servlet-api、lombok |
| runtime | 无 | 有 | 是 | MySQL 驱动 |
| test | 测试有 | 无 | 否 | JUnit、Mockito |
| system | 有 | 有 | 否 | 本地 jar,已不推荐 |
| import | — | — | — | 仅在 dependencyManagement 导入 BOM |
记两个最常用的:provided 表示“编译时需要,运行时由容器提供”,所以 Tomcat 已经带了 servlet-api,项目里就不该再打一份;runtime 表示“写代码时用不到,跑起来才需要”,最典型的是 JDBC 驱动,代码里只出现 java.sql 的接口。
依赖冲突怎么解
Maven 选版本有两条规则:最短路径优先,路径一样长时先声明的优先。所以同一个 jar 出现多个版本时,最终生效的往往不是你最想要的那个。常见手段有三种:
<!-- 1. 排除传递进来的旧版本 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>common-web</artifactId>
<version>1.2.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 2. 在 dependencyManagement 里统一版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-core</artifactId>
<version>${common.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
第三种是导入 BOM。如果项目没有继承 spring-boot-starter-parent,而是用了自定义父 POM,可以把 Spring Boot 的 BOM 导进 dependencyManagement:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
多模块与继承
项目一大就会拆模块,比如 demo-common、demo-service、demo-web。拆完之后要有一个父 POM 统一管理版本和插件:
<groupId>com.example</groupId>
<artifactId>demo-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>demo-common</module>
<module>demo-service</module>
<module>demo-web</module>
</modules>
父 POM 的 packaging 必须是 pom,否则打包会失败。子模块用 <parent> 指向父 POM,只写自己需要的依赖,版本尽量交给父 POM 的 dependencyManagement。
多模块最容易出问题的地方是“以为父 POM 管住了版本”。只要子模块自己写了 <version>,父 POM 的管理就不再生效;还有一种情况是子模块之间互相依赖,demo-service 依赖 demo-common 却没写版本,父 POM 里也没有对应条目,构建时直接报缺少版本。
父 POM 继承是单继承,子模块只能有一个 <parent>。需要同时使用 Spring Boot 的依赖管理时,通常让父 POM 继承 spring-boot-starter-parent,或者用 import 方式导入 BOM,不要两边都写一套版本。
聚合和继承是两回事:<modules> 让父项目能一次构建所有子模块,<parent> 让子模块继承父项目的配置。父 POM 里同时写 modules 和 dependencyManagement,是最常见的做法。
公司项目还要配私服。在 settings.xml 里配 <mirrors> 和 <servers>,密码不要明文写在 POM 里。私服连不上时构建会卡在下载依赖,加上 -s 指定 settings 文件、-X 看调试日志,能快速定位是网络还是认证问题。
多模块项目建议在根目录执行 mvn clean install,先把 demo-common 装进本地仓库,再单独启动 demo-web。只 package 某个子模块时,它依赖的其他模块可能还是旧版本,现象是“代码明明改了,运行还是老逻辑”。
常见坑
1. provided 的依赖运行时找不到
本地写个 main 方法测试,启动就报 ClassNotFoundException,因为 provided 不参与运行 classpath。判断标准很简单:这个 jar 是容器/服务器提供的,还是应用自己启动需要的?前者用 provided,后者用 compile。
2. 依赖写进 dependencies 却用 import scope
<scope>import</scope> 只能出现在 dependencyManagement 里,而且必须配 <type>pom</type>。放在普通 dependencies 里会报 “import scope can only be used in dependencyManagement”。
3. test 依赖被打进生产包
test scope 不会进 target/*.jar,但如果测试工具类被放进了 src/main/java,编译主代码时就会找不到依赖。测试代码和测试依赖都放在 src/test 下。
4. system scope 写死本地路径
<scope>system</scope> 配合 <systemPath> 指向 D:\lib\xxx.jar,换台机器就构建失败。老 jar 要么传到私服,要么用 mvn install:install-file 装进本地仓库。
5. SNAPSHOT 一直拿旧的
SNAPSHOT 版本默认每天更新一次,当天别人重新发布也拿不到。需要强制刷新时加 -U:
mvn -U clean package
6. 父 POM 的版本没生效
子模块自己又写了一遍 version,dependencyManagement 就管不住它。子模块只写 groupId 和 artifactId,版本交给父 POM 统一管理。
排查步骤
# 看完整依赖树,找同一个 jar 的多个版本
mvn dependency:tree -Dverbose
# 只看某个 groupId/artifactId
mvn dependency:tree -Dincludes=com.google.guava:guava
# 看最终生效的 POM(合并父 POM、profile 之后)
mvn help:effective-pom
# 强制更新 SNAPSHOT
mvn -U clean package
报 NoSuchMethodError、NoClassDefFoundError 时,先别改代码,用上面的命令确认实际生效的 jar 版本,再决定排除谁。
评论