ElIauk

Maven 依赖管理与 scope 踩坑

三坐标与依赖树 Maven 用 groupId、artifactId、version 三个坐标定位一个 jar。本地仓库默认在 ~/.m2/repository,公司一般会再配一个私服。项目里直接写的依赖只是一部分,更多 jar 是通过“依赖传递”间接进来的:A 依赖 B,B 依赖 C,那么 A 的

评论
Maven 依赖管理与 scope 踩坑

三坐标与依赖树

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 版本,再决定排除谁。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者