0%

tk.mybaits和mybatisPlus兼容性

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Class<?> getEntityClass(MappedStatement ms) {
String msId = ms.getId();
if (entityClassMap.containsKey(msId)) {
return entityClassMap.get(msId);
} else {
Class<?> mapperClass = getMapperClass(msId);
Type[] types = mapperClass.getGenericInterfaces();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType t = (ParameterizedType) type;
if (t.getRawType() == this.mapperClass || this.mapperClass.isAssignableFrom((Class<?>) t.getRawType())) {
// 报错报在这里
Class<?> returnType = (Class<?>) t.getActualTypeArguments()[0];
//获取该类型后,第一次对该类型进行初始化
EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());
entityClassMap.put(msId, returnType);
return returnType;
}
}
}
}
throw new MapperException("无法获取 " + msId + " 方法的泛型信息!");
}

这里直接下结论:报这个错的原因是因为指定扫描的路径范围太大了,导致扫描到了底层中的二次封装类public interface CommonMapper<T> extends Mapper<T>,结合以上代码不难发现,是取出了T泛型进行强转之后报错,所以只需要将扫描路径的范围缩小即可。

扩展

​ 在最后发现问题之后,解决起来就很轻松了,但是在排查的过程中,就非常的痛苦。因为报错的这个地方并没有显式的指明是某个类强转失败,而项目中又存在mybatisPlustk.mybatis共存的情况,乍一看代码以为是兼容性的问题,因为对于这两种方式来说,数据访问层接口采用的是不同的继承接口。

一个是tk.mybatis.mapper.common.Mapper,一个是com.baomidou.mybatisplus.core.mapper.BaseMapper,所以一看到这个类型转换异常,就误以为是因为需要通过tk.mybatis来解析的xxxMappermybatisPlus接管导致。

​ 在排查跟踪源码的过程中,对tk.mybatismybatisPlus相关的一些处理有了进一步的了解,在此做了记录。

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时,将它的resolvedTargetTypebeanClass字段都标记为了tk.mybatis.spring.mapper.MapperFactoryBean,而mybatis扫描器扫描器中这两个字段标记的是org.mybatis.spring.mapper.MapperFactoryBean,这就导致在真正实例化Spring bean对象时调用了不同的后置接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 篇幅原因省略部分代码
// AbstractAutowireCapableBeanFactory#invokeInitMethods
protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd){

boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
((InitializingBean) bean).afterPropertiesSet();
return null;
}, getAccessControlContext());
}
}
else {
// 最后会调用初始化的后置接口
((InitializingBean) bean).afterPropertiesSet();
}
}
}

接着跟踪下进入到不同的后置接口的调用方式,继承关系如下代码所示,从上面的代码中的最后调用bean对象的afterPropertiesSet方法,到下面代码中是由于二者的上层接口中DaoSupport实现了InitializingBean方法。且这个后置初始化方法中仅调用了checkDaoConfiginitDao方法,均由具体的子类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// tk.mybatis.spring.mapper.MapperFactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{}

// org.mybatis.spring.mapper.MapperFactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{}

public abstract class SqlSessionDaoSupport extends DaoSupport{}


public abstract class DaoSupport implements InitializingBean {
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// Let abstract subclasses check their configuration.
checkDaoConfig();

// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
}

tk.mybatis.spring.mapper.MapperFactoryBean具体的实现:核心的处理在这里,刚刚上文说到,tk.mybatis会将指定路径下的所有@Mapper类的BeanDefinition中的resolvedTargetTypebeanClass都标记为tk.MapperFactoryBean,那么也就是说指定路径下的所有@Mapper都会被识别为是tk.mybatismapper?其实不是,这里有一个判断mapperHelper.isExtendCommonMapper(),这个判断的主要逻辑是获取这个XxxMapper接口及所有上层父类接口是否有tk.mybatis.mapper.annotation.RegisterMapper注解,如果没有,则不采用tk.mybatis的方式进行解析。到这,应该就可以解答前面提出的问题,所以如果需要用mybatisPlus,那么它的XxxMapper接口的所有上传父类接口必然不会存在tk.RegisterMapper注解,也就不会进行tk.mybatis相关的处理(tk.mybatis这里的处理是将表实体和xxxMapper接口的一些方法等信息缓存起来)。

1
2
3
4
5
6
7
8
@Override
protected void checkDaoConfig() {
// 省略部分代码..

if (configuration.hasMapper(this.mapperInterface) && mapperHelper != null && mapperHelper.isExtendCommonMapper(this.mapperInterface)) {
mapperHelper.processConfiguration(getSqlSession().getConfiguration(), this.mapperInterface);
}
}

总结

  1. No qualifying bean of type 'com.xxxx'..的实际原因是移植的功能模块类路径不落在默认扫描路径范围内(启动类所在的包及子包)
  2. 无法获取实体类com.xx.xxx...xxx对应的表名是因为通过org.mybatis.spring.annotation.MapperScan指定了mybatis的扫描路径,但没有指定tk.MapperScan,导致tk的扫描器使用默认值(默认值为启动类所在的包及子包),恰好移植的功能模块不属于启动类的包及子包中,所以移植功能中的所有xxxMapper都被解析为了mybatis类型(也就是前面说的beanDefinition中的那两个属性被赋值为了mybatis对应的类),最后导致在初始化bean对象时,没有进入到tk.mybaits对应类的后置处理器,也就没有将这些表实体相关信息缓存,从而导致以上报错。
  3. 类型转换异常的报错,是因为指定tk.MapperScan设置扫描范围太大,导致扫描到了底层二次封装的抽象父类,导致在获取接口层参数获取到了泛型参数T,泛型参数以T进行类型转换为Class从而导致了报错。
  4. 在都不指定@MapperScan的情况下,二者扫描器的默认扫描路径都是启动类所在的包以及子包,当指定了tk.@MapperScan之后那么tk.mybatis的扫描路径变成了这个具体的指定路径,mybatis的扫描路径还是默认的启动类所在的包极其子包,也就是说两个扫描器的扫描路径是互不影响的、各自维护,这个很重要,如果不明白这个,在排查的过程中,会影响对问题的判断。
  5. 引入tk.mybatis一定会经过两次扫描器,一次是tk.mybatis的扫描器,一次是mybatis原生的扫描器,如果某个mapper对象被tk.mybatis扫描生成了BeanDefinitionmybatis的扫描器会跳过该类,同样的,某个xxxMapper若想使用mybatisPlus的方式进行数据访问层的处理,虽然它在扫描阶段被认为是以tk.mybatis的方式解析,但是在具体创建spring bean对象的时候,kt.mybatis的后置接口中有相应的逻辑判断(所有上层接口是否有标记RegisterMapper),来控制是否进行相应处理。
-------------本文结束感谢您的阅读-------------