Bootstrap

原创 | 从Spring Boot 2.x整合Mybatis-Plus深入理解Mybatis解析Mapper底层原理

点击上方蓝色“猿芯”关注我们,输入1024,你懂的 

背景

最近在使用高版本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配置类,通过其内部类AutoConfiguredMapperScannerRegistrar  的registerBeanDefinitions()注册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对象所导致。

后记

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

作者简介:编筐少年一枚简单的北漂程序员。喜欢用简单的文字记录工作与生活中的点点滴滴,愿与你一起分享程序员灵魂深处真正的内心独白。我的微信号:WooolaDunzung,公众号【猿芯输入 1024 ,有份惊喜送给你哦

< END > 

;