Spring Boot遇上Maven依赖冲突:打怪升级全攻略

4 阅读13分钟

一、项目报错,冲突初现

家人们,谁懂啊!最近在搞一个超有挑战性的 Spring Boot 项目,本想着大干一场,结果项目启动的时候就给我来了个下马威!一堆报错信息直接糊我脸上,项目根本启动不起来。

看着控制台那密密麻麻的红色报错信息,我的心都凉了半截。仔细一看,好家伙,全是什么ClassNotFoundException,还有各种方法找不到的错误。这可把我整懵了,代码我都反复检查过了,逻辑上没啥问题啊,怎么就启动不了呢?

经过我一番地毯式的排查,终于发现了罪魁祸首 ——Maven 依赖冲突!原来,不同的依赖包之间对某些相同的库有着不同的版本需求,这就导致了在项目运行时,Maven 不知道该听谁的,从而引发了一系列的问题。

我深知,解决这个问题迫在眉睫,不然项目根本没法继续推进。于是,我踏上了漫长而又艰辛的依赖冲突排查之旅。在这个过程中,我遇到了无数的坑,也收获了满满的经验。今天,我就把这些经验毫无保留地分享给大家,希望能帮助大家在遇到类似问题时少走弯路,快速解决问题。

二、依赖冲突,究竟为何

在深入解决问题之前,我们先来搞清楚 Maven 依赖冲突到底是怎么一回事。Maven 依赖冲突,简单来说,就是当你的项目中多个依赖包引入了同一个 jar 包的不同版本时,就可能会引发冲突。这就好比一场混乱的派对,不同的人都想带自己喜欢的饮料来,结果发现大家带的同一种饮料有不同的品牌和口味,最后都不知道该喝哪一种了。

Maven 在处理依赖时,有一套自己的依赖解析机制。它主要遵循两个原则:最近路径原则和声明优先原则。最近路径原则,也叫层级优先原则,就是说 Maven 会优先选择依赖路径最短的那个版本。比如说,项目 A 依赖 B,B 依赖 C(版本 1.0),同时项目 A 又直接依赖 C(版本 2.0),那么 Maven 会选择版本 2.0 的 C,因为从 A 到 C 的直接路径比通过 B 依赖 C 的路径更短。

而声明优先原则呢,是在依赖路径长度相等的情况下,Maven 会选择在 pom.xml 文件中先声明的那个依赖版本。例如,项目 A 依赖 B(版本 1.0),又依赖 C,C 也依赖 B(版本 2.0),这时候如果在 pom.xml 中 A 对 B 的依赖声明在 C 对 B 的依赖声明之前,那么就会选择版本 1.0 的 B。

这两个原则虽然在大多数情况下能帮助 Maven 合理地选择依赖版本,但有时候也会导致一些意想不到的问题。比如说,当不同的依赖包对同一个依赖的版本需求差异较大时,Maven 选择的版本可能并不是我们项目实际需要的版本,这就会引发各种异常,比如我们之前遇到的ClassNotFoundException

再举个具体的例子,假设我们的项目中引入了两个不同的模块,模块 A 依赖spring-core的版本是 5.0.0,模块 B 依赖spring-core的版本是 5.1.0。由于这两个模块的依赖路径长度可能相同,Maven 就会根据声明优先原则来选择其中一个版本。如果选择的版本与项目中其他部分的代码不兼容,就会导致各种奇怪的错误出现。 所以,了解 Maven 依赖冲突的原因和依赖解析机制,是我们解决问题的关键一步。只有清楚了问题的根源,我们才能有的放矢,找到有效的解决办法。

三、排查之路,步步为营

(一)Maven 命令显神通

Maven 为我们提供了一些非常实用的命令,这些命令就像是我们排查依赖冲突的得力助手。其中,mvn dependency:tree命令可以打印出项目的依赖树,通过这棵依赖树,我们能清晰地看到每个依赖的层级关系以及它们的版本信息,从而快速定位冲突的依赖。

比如说,在项目根目录下执行mvn dependency:tree命令,控制台会输出一大串信息,这就是我们项目的依赖树。我们可以在这堆信息中搜索那些出现多次且版本不同的依赖。假设我们发现commons-io这个依赖出现了两个版本,2.11.0 和 2.14.0,像下面这样:


[INFO] com.example:my-project:jar:1.0.0
[INFO] +- org.example:example-artifact:jar:1.0.0:compile
[INFO] |  \- commons-io:commons-io:jar:2.11.0:compile
[INFO] \- com.example:another-artifact:jar:2.0.0:compile
[INFO]    \- commons-io:commons-io:jar:2.14.0:compile

从这个输出中,我们一眼就能看出commons-io存在版本冲突,这就为我们解决问题指明了方向。

除了dependency:tree命令,mvn dependency:analyze命令也很有用。它就像是一个 “依赖侦探”,可以分析项目的依赖,找出那些未使用的依赖或者潜在的冲突依赖。执行这个命令后,Maven 会检查项目中的所有依赖,然后告诉我们哪些依赖可能存在问题。比如,如果有一些依赖在项目中根本没有被使用到,它会给我们列出一个清单,提醒我们可以考虑移除这些依赖,这样还能减小项目的体积呢。同时,如果存在一些依赖版本之间可能存在冲突,它也会给出相应的警告信息,让我们提前做好准备,避免在项目运行时出现问题。

(二)IDE 工具来助力

除了 Maven 命令,我们常用的 IDE 工具也能帮上大忙。以我最爱的 IDEA 为例,它为我们提供了非常直观的依赖查看方式。当我们打开项目中的pom.xml文件后,只需右键点击文件内容,然后选择 “Show Dependencies” 选项 ,IDEA 就会像变魔术一样弹出一个可视化的依赖树界面。

在这个界面中,冲突的 Jar 包会被 IDEA 用红色下划线特别标注出来,就像在人群中给它们贴上了显眼的标签,让我们想不注意到都难。而且,当我们把鼠标悬停在这些冲突的依赖上时,还能看到具体的版本信息以及它们是通过哪些依赖路径被引入到项目中的。如果我们想进一步查看某个依赖在pom.xml文件中的具体声明位置,只需要点击对应的路径,IDEA 就会自动帮我们定位到那里,是不是超级方便!

这种可视化的方式相比纯文本的依赖树,更加直观易懂,大大提高了我们排查冲突的效率。我们可以一眼看到项目中所有依赖的关系,快速找到那些 “捣乱” 的冲突依赖,就像在地图上找到目的地一样轻松。

(三)报错日志藏线索

报错日志也是我们排查依赖冲突的重要线索来源。当项目启动报错时,那些密密麻麻的报错信息其实就像是一个个小提示,在告诉我们问题出在哪里。我们要仔细阅读报错日志,特别是那些提到找不到类或者方法的信息。

比如说,报错日志中出现了ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper,这就说明项目在运行时找不到ObjectMapper这个类,而这个类是属于jackson-databind这个 Jar 包的。我们就可以根据这个线索,去排查jackson-databind这个 Jar 包的版本是否存在冲突。我们可以使用前面提到的 Maven 命令或者 IDE 工具,查看项目中引入的jackson-databind的版本情况,看看是不是有多个不同版本的jackson-databind被引入了,从而导致了类找不到的问题。

通过 Maven 命令、IDE 工具以及报错日志这三种方法,我们就像是拥有了三把锋利的宝剑,可以从不同的角度对依赖冲突进行全方位的排查,为我们解决问题打下坚实的基础。

四、解决之道,各显身手

经过一番艰苦的排查,终于找到了依赖冲突的根源,接下来就是想办法解决问题啦。解决 Maven 依赖冲突的方法有很多种,下面我就给大家介绍几种常用且有效的方法。

(一)依赖排除精准打击

依赖排除是解决依赖冲突最直接的方法之一。当我们发现某个依赖的传递性依赖中存在冲突版本时,就可以使用<exclusions>标签将冲突的依赖排除掉。

比如说,我们在排查时发现项目中引入的spring-boot-starter-data-jpa依赖了hibernate-core的版本 A,而另一个模块引入的some-library也依赖了hibernate-core,但版本是 B,这就导致了冲突。我们可以在引入some-library的依赖时,使用<exclusions>标签将其传递的hibernate-core依赖排除掉,如下所示:


<dependency>
    <groupId>com.example</groupId>
    <artifactId>some-library</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

这样,some-library就不会再引入hibernate-core,项目中就只会使用spring-boot-starter-data-jpa引入的hibernate-core版本,从而避免了冲突。不过在使用依赖排除时,一定要谨慎操作,确保被排除的依赖不会影响项目的其他功能。我们需要对项目的依赖关系有深入的了解,知道排除某个依赖后会不会引发其他问题。如果不确定,可以先在测试环境中进行测试,确认没有问题后再应用到生产环境中。

(二)版本锁定一统江湖

<dependencyManagement>中统一管理依赖版本,也是一种非常有效的解决依赖冲突的方法。通过这种方式,我们可以确保项目中的所有模块都使用相同版本的依赖,避免因为版本不一致而引发的冲突。

在父项目的pom.xml文件中,我们可以在<dependencyManagement>标签内定义各种依赖的版本,例如:


<dependencyManagement>
    <dependencies>
        <!-- 统一Spring Boot版本 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- 统一Jackson版本 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-bom</artifactId>
            <version>2.15.3</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

在子模块中引入依赖时,就不需要再指定版本号了,Maven 会自动使用<dependencyManagement>中定义的版本。比如在子模块中引入jackson-databind依赖:


<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

这样,整个项目中使用的jackson-databind版本就会统一为2.15.3,有效地避免了版本冲突。这种方法尤其适用于多模块项目,通过在父项目中集中管理依赖版本,可以大大提高项目的可维护性和稳定性。

(三)插件强制保驾护航

maven-enforcer-plugin插件可以在项目构建过程中强制检查依赖版本的一致性,一旦发现有依赖版本冲突,就会立即中止构建,从而帮助我们及时发现和解决问题。

我们需要在项目的pom.xml文件中配置maven-enforcer-plugin插件,如下所示:


<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.0.0</version>
    <executions>
        <execution>
            <id>enforce-dependency-convergence</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <dependencyConvergence />
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

配置好插件后,当我们执行mvn clean install等构建命令时,maven-enforcer-plugin就会发挥作用,检查项目中的依赖版本是否一致。如果发现有冲突,就会在控制台输出详细的错误信息,告诉我们哪些依赖存在版本冲突,我们就可以根据这些信息去调整项目的依赖配置,解决冲突问题。使用这个插件就像是给项目加上了一道保险,能够在开发过程中及时发现潜在的依赖冲突,避免问题在生产环境中爆发,保障项目的稳定运行。

五、实战演练,攻克难题

光说不练假把式,下面我就给大家分享一个我在实际项目中解决依赖冲突的案例,让大家更直观地感受一下整个过程。

在之前的一个电商项目中,我们使用 Spring Boot 搭建后端服务。项目启动时,突然报错:


java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.JsonNode

看到这个错误,我第一反应就是可能存在jackson-databind的依赖冲突。于是,我先用mvn dependency:tree命令查看依赖树,果然发现jackson-databind出现了两个版本,2.12.5 和 2.13.0。


[INFO] com.example:my-ecommerce-project:jar:1.0.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.6.3:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compile
[INFO] \- com.example:third-party-sdk:jar:1.0.0:compile
[INFO]    \- com.fasterxml.jackson.core:jackson-databind:jar:2.12.5:compile

从依赖树中可以看出,spring-boot-starter-web引入了 2.13.0 版本的jackson-databind,而我们项目中引入的一个第三方 SDK 引入了 2.12.5 版本的jackson-databind,这就导致了冲突。

接下来,我尝试使用依赖排除的方法来解决问题。我在引入第三方 SDK 的依赖中,排除了它传递的jackson-databind依赖:


<dependency>
    <groupId>com.example</groupId>
    <artifactId>third-party-sdk</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
    </exclusions>
</dependency>

修改完pom.xml后,我重新启动项目,结果还是报错。这可把我搞懵了,难道是排除的方式不对?我又仔细检查了一遍pom.xml,发现没有问题。

后来,我突然想到,是不是还有其他地方引入了jackson-databind的低版本呢?于是,我再次使用mvn dependency:tree命令,这次我加上了-Dverbose参数,让输出结果更加详细。经过一番仔细查看,我发现另一个模块中引入的一个工具类库,也间接引入了 2.12.5 版本的jackson-databind

找到问题后,我在这个工具类库的依赖中也进行了jackson-databind的排除:


<dependency>
    <groupId>com.example</groupId>
    <artifactId>utility-library</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
    </exclusions>
</dependency>

再次启动项目,终于成功了!那一刻,我的心情无比激动,所有的努力都没有白费。

通过这个实战案例,我深刻体会到了解决依赖冲突需要耐心和细心。有时候,问题可能不是一次就能解决的,需要我们不断地排查、尝试不同的方法。希望这个案例能给大家带来一些启发,在遇到类似问题时能够快速解决。

六、经验总结,防患未然

在解决 Spring Boot 项目中 Maven 依赖冲突的过程中,我们积累了宝贵的经验。首先,依赖冲突的排查需要综合运用多种方法,Maven 命令、IDE 工具和报错日志各有优势,相互结合才能更全面地发现问题。在解决冲突时,要根据具体情况选择合适的方法,依赖排除、版本锁定和插件强制等方法都有其适用场景。

为了避免在未来的项目中陷入依赖冲突的困境,我们在日常开发中要养成良好的习惯。遵循官方推荐的依赖版本是非常重要的,官方通常会对依赖版本进行严格的测试和兼容性验证,使用官方推荐版本可以大大降低依赖冲突的风险。定期检查项目的依赖树也是必不可少的,通过mvn dependency:tree等命令,我们可以及时发现潜在的依赖冲突,提前做好预防措施。在引入新的依赖时,一定要仔细评估其对项目现有依赖的影响,避免盲目引入导致冲突。

希望大家通过这篇文章,能够对 Spring Boot 项目中的 Maven 依赖冲突有更深入的理解,掌握有效的排查和解决方法。在未来的开发中,遇到依赖冲突时不再慌张,能够迅速找到问题的根源并解决它,让我们的项目开发更加顺利。如果大家在实际操作中有任何问题或者心得,欢迎在评论区留言分享,让我们一起共同进步!