深入理解Mybatis解析Mapper底层原理

用户4172423 2021-01-05 16:57:07
mybatis 解析 深入 入理 理解


背景

最近在使用高版本Spring Boot 2.x整合mybatis-plus 3.4.1时,控制台出现大量的warn提示XxxMapper重复定义信息:Bean already defined with the same name

2020-12-07 19:37:26.025 WARN 25756 --- [ main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'roleMapper' and 'com.dunzung.java.spring.mapper.RoleMapper' mapperInterface. Bean already defined with the same name!
2020-12-07 19:37:26.025 WARN 25756 --- [ main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userMapper' and 'com.dunzung.java.spring.mapper.UserMapper' mapperInterface. Bean already defined with the same name!
2

虽然这些警告并不影响程序正确运行,但是每次启动程序看到控制台输出这些警告日志信息,心情不是很美丽呀。

问题分析开挂模式

Maven 依赖

Bean already defined with the same name警告信息来看,感觉应该是:重复加载 mapper 的 bean 对象定义了。所以我从mybatis-pluspom依赖入手,找到mybatis-plus总共依赖三个 jar 包:

  1. mybatis-plus-boot-starter 3.4.1
  2. mybatis-plus-extension 3.4.1
  3. pagehelper-spring-boot-starter 1.2.10

接着,看了下 mybatis-plus 启动相关配置,发现也没啥毛病。

mybatis-plus 配置类

@Configuration
@MapperScan(basePackages = "com.dunzung.**.mapper.**")
public class MybatisPlusConfiguration {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
return paginationInterceptor;
}
}

Service 类定义

自定义的MybatisServiceImpl继承了mybatis-plusServiceImpl实现类;自定义的MybatisService继承了IService接口类。

/**
* 自定义 Service 接口基类
*/
public interface MybatisService<T> extends IService<T> {
}
public interface RoleService extends MybatisService<RoleEntity> {
}
/**
* 自定义 Service 实现接口基类
*/
public class MybatisServiceImpl<M extends DaoMapper<T>, T> extends ServiceImpl<M, T> implements MybatisService<T> {
}
@Slf4j
@Service
public class RoleServiceImpl extends MybatisServiceImpl<RoleMapper, RoleEntity> implements RoleService {
}

Mapper 类定义

RoleMapper基于注解@Mapper配置,基本上零配置(xml)。

@Mapper
public interface RoleMapper extends DaoMapper<RoleEntity> {
}

上面的 mybatis-plus 相关配置非常简单,没啥毛病,所以只能从 mybatis-plus 相关的三个jar源码入手了。

祖传源代码分析

从日志输出信息定位可以看出是o.m.s.mapper.ClassPathMapperScanner打印的警告日志,于是在ClassPathMapperScanner类中找到了输出警告日志的checkCandidate()方法:

 /**
* {@inheritDoc}
*/
@Override
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
if (super.checkCandidate(beanName, beanDefinition)) {
return true;
} else {
LOGGER.warn(() -> "Skipping MapperFactoryBean with name '" + beanName + "' and '"
+ beanDefinition.getBeanClassName() + "' mapperInterface" + ". Bean already defined with the same name!");
return false;
}
}
}

打开Debug模式,在ClassPathMapperScannercheckCandidate()方法体打断点,验证该方法是否重复调用两次。

  • 第一次Spring Boot程序启动时会自动装配mybatis-spring-boot-autoconfigure这个jar包中的MybatisAutoConfiguration配置类,通过其内部类AutoConfiguredMapperScannerRegistrarregisterBeanDefinitions()注册bean方法,调用了ClassPathMapperScannerdoScan() 方法,然后通过checkCandidate()方法判断mapper对象是否已注册。

doScan方法详细代码如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
...
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
...
if (checkCandidate(beanName, candidate)) {
...
}
}
}

Tips

checkCandidate()对已注册mapper对象进行是否重复定义判断

  • 第二次通过MapperScans注解,通过@Import注解,导入并调用了mybatis-spring-2.0.5这个jar包中MapperScannerConfigurer类的postProcessBeanDefinitionRegistry()方法,在postProcessBeanDefinitionRegistry()方法中 再一次实例化mapper的扫描类ClassPathMapperScanner,并又一次调用doScan方法初始化mapper对象,且也调用了checkCandidate()方法,从而有了文章开头日志输出的Bean already defined with the same name警告信息。
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
...
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

Debug调试到这里,大致猜到是mybatis-plus相关jar包有bug了,主要涉及两个jar

  • 第一个是mybatis-spring-boot-autoconfigure,主要是用于spring自动装配mybatis相关初始化配置,mybatis自动装配配置类是MybatisAutoConfiguration
  • 第二个是mybatis-spring,从http://mybatis.org/官网可知,这个包是mybatisspring结合具备事务管理功能的数据访问应用程序包,涉及到数据库操作,如数据源(DataSoure),操作 SqlSqlSessionFactory工厂类,以及 初始化MapperMapperFactoryBean工厂类等等。

解决问题我是有原则的

从上面的debug调试代码分析可以得出,mapper确实被实例化了2次,也验证了我当初的判断。

那为什么会这样呢?

我们不妨先把工程依赖的pagehelper-spring-boot-starter升级最新版到1.3.0版本,mybatis-plus-boot-startermybatis-plus-extension已经是最新版本3.4.1,再次Application启动警告尽然自动消失了。

这里我对比了在mybatis-spring-boot-autoconfigure包中MybatisAutoConfiguration所属内部类 AutoConfiguredMapperScannerRegistrarregisterBeanDefinitions()方法,发现1.3.2版本和2.1.3版本的代码实现区别非常大,几乎是重写了该方法。

mybatis-spring-boot-autoconfigure 的 1.3.2 版本写法

/**
* This will just scan the same base package as Spring Boot does. If you want
* more power, you can explicitly use
* {@link org.mybatis.spring.annotation.MapperScan} but this will get typed
* mappers working correctly, out-of-the-box, similar to using Spring Data JPA
* repositories.
*/
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
logger.debug("Searching for mappers annotated with @Mapper");
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
for (String pkg : packages) {
logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException ex) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
}
}
}
/**
* {@link org.mybatis.spring.annotation.MapperScan} ultimately ends up
* creating instances of {@link MapperFactoryBean}. If
* {@link org.mybatis.spring.annotation.MapperScan} is used then this
* auto-configuration is not needed. If it is _not_ used, however, then this
* will bring in a bean registrar and automatically register components based
* on the same component-scanning path as Spring Boot itself.
*/
@org.springframework.context.annotation.Configuration
@Import({ AutoConfiguredMapperScannerRegistrar.class })
@ConditionalOnMissingBean(MapperFactoryBean.class)
public static class MapperScannerRegistrarNotFoundConfiguration {
@PostConstruct
public void afterPropertiesSet() {
logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
}

mybatis-spring-boot-autoconfigure 的 2.1.3 版本写法

@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public MapperScannerRegistrarNotFoundConfiguration() {
}
public void afterPropertiesSet() {
MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}
}
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
} else {
MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
packages.forEach((pkg) -> {
MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
});
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors()).filter((x) -> {
return x.getName().equals("lazyInitialization");
}).findAny().ifPresent((x) -> {
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
});
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
}
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
}

1.3.22.1.3源码对比可以看出:

2.1.3版本中,在MapperScannerRegistrarNotFoundConfiguration类的条件注解@ConditionalOnMissingBean加上了MapperScannerConfigurer.class这个mapper配置扫描类判断。

也就是说在bean容器中,只会存在一个单例的MapperScannerConfigurer对象,并且只会在spring容器注册bean的时候,通过postProcessBeanDefinitionRegistry()方法初始化一次mapper对象,不像1.3.2版本那样通过不同的类两次去实例化ClassPathMapperScanner类,重新注册mapper对象。

而造成不一致的直接原因是mybatis-plus-extensionpagehelper-spring-boot-starter共同依赖的mybatis-spring的版本不一致导致的。

mybatis-plus-extension依赖的是mybatis-spring2.0.5版本

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
<scope>compile</scope>
</dependency>

pagehelper-spring-boot-starter依赖的是mybatis-spring1.3.2版本

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>

所以由上总述,知道了问题产生的原因,解决办法就很简单了,只需要把pagehelper-spring-boot-starter的版本升级到1.3.0即可。

有态度的良心总结

虽然提示Bean already defined with the same name警告信息的直接原因是pagehelper-spring-boot-startermybatis-plus-extension共同依赖的mybatis-spring的版本不一致导致。

但根本原因在于MapperScannerConfigurerAutoConfiguredMapperScannerRegistrar类中两次实例化ClassPathMapperScanner对象注册mapper对象所导致。

后记

在实际的生产环境中,每次开源框架级别的升级,要特别注意框架所依赖的版本对应关系,最好的办法是去相关开源框架的官网了解具体的版本升级博客文章或升级日志,避免带来不必要的麻烦和损失。

本文分享自微信公众号 - 日拱一兵(gh_6235a38420b9)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间: 2020-12-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

版权声明
本文为[用户4172423]所创,转载请带上原文链接,感谢
https://cloud.tencent.com/developer/article/1768064

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云