tk.mybaits和mybatisPlus兼容性
背景
项目早期建设时底层采用了tk.mybatis
方式来对数据访问层进行数据处理,而后再更新Springboot
的同时因为一些不知原因(只能说我级别不够,不知领导的深意,但确实tk.mybatis
官网已经没有维护了)又在底层引入了mybatisPlus
,所以新代码采用mybatisPlus
编写,旧模块任然使用tk.mybatis
编写。
问题
今天同事在移植项目中的一个功能模块时,报了一个错No qualifying bean of type 'com.xxxx'..
。乍一看猜测大概是因为对应的xxxMapper
接口文件没有被扫描到Spring
容器中,导致tk.mybatis
的表集合中没有该实体。排查发现,启动类中并没有显示使用@MapperScan
指定扫描路径,果然,使用的是启动类及以下的包结构,由于是搬迁的功能模块,不想调整原来包结构,故使用@MapperScan
来指定扫描路径。
在启动类上通过@MapperScan
指定了扫描路径之后可以正常启动,但是在调用接口发现又报错了无法获取实体类com.xx.xxx...xxx
对应的表名,如果对tk.mybatis
比较熟悉就知道,这里是因为tk.mybatis
会将所有的表信息都缓存到一个集合中,仔细检查了指定的路径,也没有发现问题。
解决
经过一段时间的排查,终于找到了原因,在这里记录下,避免以后又遇到同样的问题,以下仅为个人观点,可能存在理解错误,请带着批判的视角阅读。
原来是在tk.mybatis
中也有一个同名的tk.mybatis.spring.annotation.MapperScan
,若使用org.mybatis.spring.annotation.MapperScan
是不会对扫描到的对象进行一些tk.mybatis
相关的处理,所以直接将注解更换为前者,再次启动,又报出了新的错误:
定位进去发现就是tk.mybatis
处理对应接口缓存的时候报了类型转换异常:
Caused by: java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl cannot be cast to java.lang.Class
1 | public Class<?> getEntityClass(MappedStatement ms) { |
这里直接下结论:报这个错的原因是因为指定扫描的路径范围太大了,导致扫描到了底层中的二次封装类public interface CommonMapper<T> extends Mapper<T>
,结合以上代码不难发现,是取出了T
泛型进行强转之后报错,所以只需要将扫描路径的范围缩小即可。
扩展
在最后发现问题之后,解决起来就很轻松了,但是在排查的过程中,就非常的痛苦。因为报错的这个地方并没有显式的指明是某个类强转失败,而项目中又存在mybatisPlus
和tk.mybatis
共存的情况,乍一看代码以为是兼容性的问题,因为对于这两种方式来说,数据访问层接口采用的是不同的继承接口。
一个是tk.mybatis.mapper.common.Mapper
com.baomidou.mybatisplus.core.mapper.BaseMapper
,所以一看到这个类型转换异常,就误以为是因为需要通过tk.mybatis
来解析的xxxMapper
被mybatisPlus
接管导致。
在排查跟踪源码的过程中,对tk.mybatis
和mybatisPlus
相关的一些处理有了进一步的了解,在此做了记录。
tk.mybatis
通过tk.mybatis.spring.mapper.ClassPathMapperScanner
扫描器将对应的表实体收集到一个集合中,注意mybatis
也有自己的扫描器org.mybatis.spring.mapper.ClassPathMapperScanner
,也就是说在项目启动过程中,实际上是经过了两次的扫描,并且tk.mybatis
的扫描器在mybatis
的扫描器之前,至于是怎么插到mybatis
之前的,猜测是通过spring
的某个后置接口,这个与本篇主题无关,不进一步详解。
其实tk.mybatis
的扫描器主要是将指定路径(默认是启动类所在包及子包)中被@Mapper
标记的类,解析成BeanDefinition
,添加到Spring
对应集合中,用于后续将其实例化为具体的bean
对象到容器中。这些的主要逻辑在这两个扫描器的父类ClassPathBeanDefinitionScanner#doScan
。经过tk.mybatis
扫描器处理后的表实体,同样也会在mybatis
扫描器被作为候选对象candidate
被扫描到。但它会从Spring
中查询一次,查看是否存在该BeanDefinition
,若存在则打印Skipping MapperFactoryBean with name 'xxxMapper' and 'com.xx.xxx....mapper.XxxMapper' mapperInterface. Bean already defined with the same name!
,大概意思就是这个类已经存在BeanDefinition
,当前处理跳过该类的处理,也就是说在指定路径下的实体表只会被这两个扫描器中的其中一个扫描并进行相关处理。
tk.mybatis
扫描器中在创建BeanDefinition
时,将它的resolvedTargetType
和beanClass
字段都标记为了tk.mybatis.spring.mapper.MapperFactoryBean
,而mybatis
扫描器扫描器中这两个字段标记的是org.mybatis.spring.mapper.MapperFactoryBean
,这就导致在真正实例化Spring bean
对象时调用了不同的后置接口。
1 | // 篇幅原因省略部分代码 |
接着跟踪下进入到不同的后置接口的调用方式,继承关系如下代码所示,从上面的代码中的最后调用bean
对象的afterPropertiesSet
方法,到下面代码中是由于二者的上层接口中DaoSupport
实现了InitializingBean
方法。且这个后置初始化方法中仅调用了checkDaoConfig
和initDao
方法,均由具体的子类实现。
1 | // tk.mybatis.spring.mapper.MapperFactoryBean |
tk.mybatis.spring.mapper.MapperFactoryBean
具体的实现:核心的处理在这里,刚刚上文说到,tk.mybatis
会将指定路径下的所有@Mapper
类的BeanDefinition
中的resolvedTargetType
和beanClass
都标记为tk.MapperFactoryBean
,那么也就是说指定路径下的所有@Mapper
都会被识别为是tk.mybatis
的mapper
?其实不是,这里有一个判断mapperHelper.isExtendCommonMapper()
,这个判断的主要逻辑是获取这个XxxMapper
接口及所有上层父类接口是否有tk.mybatis.mapper.annotation.RegisterMapper
注解,如果没有,则不采用tk.mybatis
的方式进行解析。到这,应该就可以解答前面提出的问题,所以如果需要用mybatisPlus
,那么它的XxxMapper
接口的所有上传父类接口必然不会存在tk.RegisterMapper
注解,也就不会进行tk.mybatis
相关的处理(tk.mybatis
这里的处理是将表实体和xxxMapper
接口的一些方法等信息缓存起来)。
1 |
|
总结
No qualifying bean of type 'com.xxxx'..
的实际原因是移植的功能模块类路径不落在默认扫描路径范围内(启动类所在的包及子包)- 无法获取实体类
com.xx.xxx...xxx
对应的表名是因为通过org.mybatis.spring.annotation.MapperScan
指定了mybatis
的扫描路径,但没有指定tk.MapperScan
,导致tk
的扫描器使用默认值(默认值为启动类所在的包及子包),恰好移植的功能模块不属于启动类的包及子包中,所以移植功能中的所有xxxMapper
都被解析为了mybatis
类型(也就是前面说的beanDefinition
中的那两个属性被赋值为了mybatis
对应的类),最后导致在初始化bean
对象时,没有进入到tk.mybaits
对应类的后置处理器,也就没有将这些表实体相关信息缓存,从而导致以上报错。 - 类型转换异常的报错,是因为指定
tk.MapperScan
设置扫描范围太大,导致扫描到了底层二次封装的抽象父类,导致在获取接口层参数获取到了泛型参数T
,泛型参数以T
进行类型转换为Class
从而导致了报错。 - 在都不指定
@MapperScan
的情况下,二者扫描器的默认扫描路径都是启动类所在的包以及子包,当指定了tk.@MapperScan
之后那么tk.mybatis
的扫描路径变成了这个具体的指定路径,mybatis
的扫描路径还是默认的启动类所在的包极其子包,也就是说两个扫描器的扫描路径是互不影响的、各自维护,这个很重要,如果不明白这个,在排查的过程中,会影响对问题的判断。 - 引入
tk.mybatis
一定会经过两次扫描器,一次是tk.mybatis
的扫描器,一次是mybatis
原生的扫描器,如果某个mapper
对象被tk.mybatis
扫描生成了BeanDefinition
,mybatis
的扫描器会跳过该类,同样的,某个xxxMapper
若想使用mybatisPlus
的方式进行数据访问层的处理,虽然它在扫描阶段被认为是以tk.mybatis
的方式解析,但是在具体创建spring bean
对象的时候,kt.mybatis
的后置接口中有相应的逻辑判断(所有上层接口是否有标记RegisterMapper
),来控制是否进行相应处理。