背景

若依框架,springboot项目,集成kafka客户端,使用spring-kafka。
若依项目是父子工程,子工程分为好几个模块。先是在admin子工程中的pom里,定义了spring-kafka的依赖,如下:

<dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.4.0.RELEASE</version>
        </dependency>

定义的版本是2.4版本。集成好后,启动项目,报错,报错信息如下:

Action:

Correct the classpath of your application so that it contains a
single, compatible version of
org.springframework.kafka.listener.ConsumerProperties

网上一搜这个错误,全是jar包冲突的错误。关键是项目里第一次引入kafka,怎么会jar包冲突呢?
然后又把上面的依赖定义到了common子工程的pom中,启动成功了。神奇的现象出现了。

问题分析

为了解决这个问题,特意在idea安装了maven-helper插件,分析了每个pom,依然没有jar包冲突的提示。那到底是什么原因呢?于是我又详细看了一下报错信息,提取到了另一个有用信息:

The following method did not exist:

org.springframework.kafka.listener.ConsumerProperties.setOnlyLogRecordMetadata(Z)V

不存在这个方法,为何不存在呢?打断点一点一点看吧。于是,在ConsumerProperties类的构造方法上打了断点。启动项目,追查问题出现的地方。

在ConsumerProperties的构造方法处进了断点,然后一步一步往下执行,最终,定位到执行到这步,出现了错误:

kafka 版本命名 kafka版本问题_spring


这就是报错说的,找不到setOnlyLogRecordMetadata方法。点进去container实体类,确实找不到这个方法。所以报错了。

那为什么kafka的依赖放在common子工程,就可以启动成功呢?将依赖写在common子工程,再打断点。

进入上图的方法后,确实有setOnlyLogRecordMetadata这个方法。很明显,这进入的不是一个类。定位到两次各自的class位置,可以看到,一次调用的是2.4版本的jar包,里面没有setOnlyLogRecordMetadata方法,所以报错了,一次调用的是2.7.3版本的jar包,这个里面是有那个方法的。

kafka 版本命名 kafka版本问题_kafka_02


那么问题又转变成了,为何jar包版本会自动变成2.7.13呢,明明定义的是2.4版本啊。而且2.4版本的jar包,为何会报错呢?

要解决这个问题,那就要想明白,为何代码里会调用一个2.4版本的jar包不存在的方法呢?这个调用的方法,是在哪里调的呢?定位到了下图:

kafka 版本命名 kafka版本问题_java_03


看到这里,问题就好解决了。是springboot的autoconfigure包里自动装配的代码,调用了setOnlyLogRecordMetadata这个方法,此项目使用的springboot的版本是2.5版本,是比较新的,所以,使用了新的kafka客户端的属性和代码,而2.4版本的kafka客户端不再此springboot版本的兼容范围内,所以2.4版本的报错了。那为啥会变成2.7.13版本呢?这是springboot集中管理的版本,在若依父工程的pom里,依赖了如下配置:

<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.5.13</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

这是springboot集中声明自动配置的jar包的版本的。每个springboot版本,都会在这里配置其自己适应的其他jar包的版本。所以,我们在定义springboot自动装配的相关jar包时,最好使用其默认规定的版本,这样就没有版本问题了。

kafka 版本命名 kafka版本问题_java_04

问题解决

通过上面的分析,可以看出,并不是jar包冲突了,而是jar包版本与springboot版本不兼容,造成的问题。因为springboot是自动装配的,所以springboot必定会自己内部实现一些相关类(如kafka)的配置和加载。而这些自动装配的类,不同版本设置是不同的,这也就造成,springboot需要指定某个版本的,去进行兼容。因此,springboot最新的版本,也就去兼容其他第三方类库的最新版本了,旧版本,旧不再兼容了。
到此,还有一个问题需要明确,那就是,为何kafka的版本定义在admin子工程,版本可以覆盖父工程中spring-boot-dependencies的版本,而定义在common子工程中,就不能覆盖呢?这就涉及到了maven的就近原则。

本级:指当前工程,即admin工程,在admin里应用了kafka
上级:指parent依赖的jar,就是若依父工程
下级:指本级引用的jar,就是common工程,admin里引入了common工程。

加载原则
当依赖一个jar包多个版本时,优先使用哪个版本,原则如下:

1.本级优先于上级,上级优先于下级;
2.本级依赖版本优先于管理版本;
3.同一个包中后加载的版本覆盖先加载的版本;
4.上级管理版本和本级管理版本会覆盖下级依赖版本;
5.不同下级jar中依赖了不同版本,优先使用先加载下级jar中的版本;
6.与版本号大小没有关系
7.本级无法使用下级管理版本

由第一个原则可知,本级admin工程优先于上级,所以,定义在admin,能直接覆盖父工程中spring-boot-dependencies定义的版本。而上级优先于下级,所以,在common中定义,无法覆盖父工程中的版本。

但是,经过本人实验,在common中,进行如下定义,也可以覆盖父工程的pom:

<dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.7.14</version>
            <type>pom</type>
        </dependency>

加上type为pom,就可以覆盖父工程的版本,去掉type是pom,就不行。博主猜测是因为在父工程中,引入spring-boot-dependencies时,定义的type是pom,所以,在common里也定义pom时,是对spring-boot-dependencies的一个覆盖。

总结

1.springboot项目,遇到某个第三方类找属性或少方法时,考虑springboot版本与第三方类版本是否兼容
2.springboot自动装配的类,使用默认版本,无特殊需求无需手动指定其版本。