前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解Mybatis解析Mapper底层原理

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

作者头像
用户4172423
发布2020-12-31 15:06:24
1.4K0
发布2020-12-31 15:06:24
举报
文章被收录于专栏:日拱一兵

背景

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

代码语言:javascript
复制
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 配置类
代码语言:javascript
复制
@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接口类。

代码语言:javascript
复制
/**
* 自定义 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)。

代码语言:javascript
复制
@Mapper
public interface RoleMapper extends DaoMapper<RoleEntity> {
}

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

祖传源代码分析

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

代码语言:javascript
复制
  /**
   * {@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方法详细代码如下:

代码语言:javascript
复制
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警告信息。
代码语言:javascript
复制
@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 版本写法

代码语言:javascript
复制
/**
   * 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 版本写法

代码语言:javascript
复制
@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版本

代码语言:javascript
复制
<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版本

代码语言:javascript
复制
<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对象所导致。

后记

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 日拱一兵 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 问题分析开挂模式
    • Maven 依赖
      • mybatis-plus 配置类
        • Service 类定义
          • Mapper 类定义
          • 祖传源代码分析
            • Tips
            • 解决问题我是有原则的
              • 那为什么会这样呢?
              • 有态度的良心总结
                • 后记
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档