百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

Mybatis-Plus 深入理解 Mapper 底层原理

lipiwang 2024-11-17 13:26 11 浏览 0 评论

背景

最近在使用高版本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

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

于是趁着最近这段空闲时间,快马加鞭动起了我的 “发财” 小手,撸起袖子加油干,花了一点时间研究了下mybatis-plus如何初始化mapper对象的相关源代码。

问题分析开挂模式

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.2和2.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-spring的2.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-spring的1.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对象所导致。

后记

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

后台私信回复 1024 免费领取 SpringCloud、SpringBoot,微信小程序、Java面试、数据结构、算法等全套视频资料。

相关推荐

前端入门——css 网格轨道详细介绍

上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...

Islands Architecture(孤岛架构)在携程新版首页的实践

一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...

HTML中script标签中的那些属性

HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...

CSS 中各种居中你真的玩明白了么

页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...

CSS样式更改——列表、表格和轮廓

上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...

一文吃透 CSS Flex 布局

原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...

css实现多行文本的展开收起

背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...

css 垂直居中的几种实现方式

前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...

WordPress固定链接设置

WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...

面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿

前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...

3种CSS清除浮动的方法

今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...

2025 年 CSS 终于要支持强大的自定义函数了?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...

css3属性(transform)的一个css3动画小应用

闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...

CSS基础知识(七)CSS背景

一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...

CSS 水平居中方式二

<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...

取消回复欢迎 发表评论: