likes
comments
collection
share

MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的

作者站长头像
站长
· 阅读数 75

前言

有天下午正在写代码,测试同学突然和我说“合集列表”出现问题,立即进行定位通过trace_id找到了ERRO日志,发现是SQL syntax error出现了两个limit,立即查看mapper.xml查看是不是合并代码时出现了冲突多合了个limit,但是发现并没有,对比了下最初写的时候的代码没看出问题,尝试本地debug看看问题,也没找到问题,尝试着通过源码来看看,点了下mapper对象,看到了interceptors属性,感觉应该是全局配置发生了变化,看了下pr发现项目中确实增加了个mybatis config:

@Configuration
@MapperScan("com.xxx.xxx.**.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }

}

正好跟分页相关了,再看看出现问题的mapper相关代码:

ist<Long> listIdsByOpen(Long uid, Integer opened, boolean homePage, Page<Long> pageParam);
public class Page<T> extends AbstractMultiLevelSortVO implements IPage<T> {
...
}
<if test="pageParam != null">
    LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</if>

看到了IPage就知道了是因为MybatisPlusConfig触发了它。

在排查的时候还发现了@MapperScan的basePackages包名写错了,但是没有影响。

下面我们整理下问题,再一一解决。

需要了解的问题

  • 为啥@MapperScan的basePackages包名写错了,但是不受影响?
  • @MapperScan是如何生效的
  • mapper接口的初始化
  • 为啥配置PaginationInterceptor了撬动了IPage导致多产生了一个limit?

为啥@MapperScan的basePackages包名写错了,但是不受影响

因为在component中有个全局的MybatisAutoConfig:

@Configuration
@ConditionalOnProperty("mybatis.mapper.scan-package")
@MapperScan("${mybatis.mapper.scan-package}")

yml中配置了:

mybatis:
  mapper:
    scan-package: com.xxx.xxx.**.mapper

另外mybatis-plus starter也有自动装配,默认装配的MapperScannerConfigurer需要配合@Mapper注释: MybatisPlusAutoConfiguration#registerBeanDefinitions

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
    }

    logger.debug("Searching for mappers annotated with @Mapper");

    List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
    if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> 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())
        // Need to mybatis-spring 2.0.2+
        .filter(x -> x.getName().equals("lazyInitialization")).findAny()
        .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
    registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

上面自己定义的,在@MapperScan中没有指定mapper接口需要被特殊注解注释,因此在写mapper接口时不需要加上@Mapper。

@MapperScan是如何生效的

mybatis-spring提供的一个注解,用于自动扫描指定包下的Mapper接口,并将其注册成Spring容器中的bean。

MapperScannerRegistrar

通过@MapperScan源码可以看到通过@Import动态注册了MapperScannerRegistrar Bean发现其为核心处理类:

package org.mybatis.spring.annotation;
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    // 获得@MapperScan注解的属性,可以看到value值为我们指定的,factoryBean为默认的MapperFactoryBean用于后面给dao bean生成代理使用。
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
          generateBaseBeanName(importingClassMetadata, 0));
    }
  }
}

可以看到:

  • 实现了ImportBeanDefinitionRegistrar,在处理@Configuration类时,可以使用它注册额外的BeanDefinitions,并将它注册到Spring容器中。

    @MapperScan通过@Import将MapperScannerRegistrar注册到了Spring容器中。

  • registerBeanDefinitions方法的入参为BeanDefinitionRegistry,所以不仅可以创建还可以删除和修改BeanDefinitions。

    这里MapperScannerRegistrar#registerBeanDefinitions的importingClassMetadata为我们的配置类——MybatisPlusConfig,BeanDefinitionRegistry为DefaultLisableBeanFactory。

继续调用的方法registerBeanDefinitions(LAnnotationMetadata,LAnnotationAttributes;LBeanDefinitionRegistry;LString)

void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
    BeanDefinitionRegistry registry, String beanName) {
  // #1
  BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
  builder.addPropertyValue("processPropertyPlaceHolders", true);
  
  // #2
  Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
  if (!Annotation.class.equals(annotationClass)) {
    builder.addPropertyValue("annotationClass", annotationClass);
  }

  Class<?> markerInterface = annoAttrs.getClass("markerInterface");
  if (!Class.class.equals(markerInterface)) {
    builder.addPropertyValue("markerInterface", markerInterface);
  }

  Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
  if (!BeanNameGenerator.class.equals(generatorClass)) {
    builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
  }

  Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
  if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
    builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
  }

  String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
  if (StringUtils.hasText(sqlSessionTemplateRef)) {
    builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
  }

  String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
  if (StringUtils.hasText(sqlSessionFactoryRef)) {
    builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
  }

  //可以看到多种方式都进行合并了
  List<String> basePackages = new ArrayList<>();
  basePackages.addAll(
      Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));

  basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
      .collect(Collectors.toList()));

  basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
      .collect(Collectors.toList()));

  if (basePackages.isEmpty()) {
    basePackages.add(getDefaultBasePackage(annoMeta));
  }

  String lazyInitialization = annoAttrs.getString("lazyInitialization");
  if (StringUtils.hasText(lazyInitialization)) {
    builder.addPropertyValue("lazyInitialization", lazyInitialization);
  }

  builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

  // #3
  registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

}
  1. 获取了一个MapperScannerConfigurer BeanDefinitionBuilder:
public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    ...
} 

可以看到MapperScannerConfigurer是一个BeanFactotyPostProcessor,从指定包下开始递归搜索接口,并将其注册为MapperFactoryBean。

  1. 获取@MapperScan的annotationClass属性,这里是默认的Annotation,因此Mapper接口无需还要有任何其他注解。
  2. 这里向BeanFactory中注册了beanName为com.onepiece.picture.MybatisPlusConfig#MapperScannerRegistrar#0,BeanDefinition为MapperScannerConfigurer。

继续往下跟MapperScannerConfigurer。

MapperScannerConfigurer

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  if (this.processPropertyPlaceHolders) {
    // #1
    processPropertyPlaceHolders();
  }
  
  // #2
  ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  scanner.setAddToConfig(this.addToConfig);
  scanner.setAnnotationClass(this.annotationClass);
  scanner.setMarkerInterface(this.markerInterface);
  scanner.setSqlSessionFactory(this.sqlSessionFactory);
  scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
  scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
  scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
  scanner.setResourceLoader(this.applicationContext);
  scanner.setBeanNameGenerator(this.nameGenerator);
  scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
  if (StringUtils.hasText(lazyInitialization)) {
    scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
  }
  // # 3
  scanner.registerFilters();
  // #4
  scanner.scan(
      StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
  1. 处理占位符获取替换为真实的值,比如下面将${mybatis.mapper.scan-package}替换为配置中的值:
@MapperScan("${mybatis.mapper.scan-package}")

2. 这里创建了一个ClassPathMapperScanner对象,传递了BeanDefinitionRegistry便于后续扫描mapper接口进行注册

ClassPathMapperScanner位于mybatis-spring包中继承了ClassPathBeanDefinitionScanner(使用提供的BeanDefinitionResgistry和classpath注册相应的bean definitions),

3. 过滤出想要的mapper接口:

添加包含、排除的过滤器,比如@MapperScan可以指定接口需要注释哪个注解、实现哪个接口
public void registerFilters() {
  boolean acceptAllInterfaces = true;

  // if specified, use the given annotation and / or marker interface
  if (this.annotationClass != null) {
    addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
    acceptAllInterfaces = false;
  }

  // override AssignableTypeFilter to ignore matches on the actual marker interface
  if (this.markerInterface != null) {
    addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
      @Override
      protected boolean matchClassName(String className) {
        return false;
      }
    });
    acceptAllInterfaces = false;
  }

  if (acceptAllInterfaces) {
    // default include filter that accepts all classes
    addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
  }

  // exclude package-info.java
  addExcludeFilter((metadataReader, metadataReaderFactory) -> {
    String className = metadataReader.getClassMetadata().getClassName();
    return className.endsWith("package-info");
  });
}

4. 进行扫描注册:

ClassPathBeanDefinitionScanner#scan:

public int scan(String... basePackages) {
   int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

   doScan(basePackages);

   // Register annotation config processors, if necessary.
   if (this.includeAnnotationConfig) {
      AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
   }

   return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

ClassPathMapperScanner#doScan

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
  // #1  
  Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

  if (beanDefinitions.isEmpty()) {
    LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
        + "' package. Please check your configuration.");
  } else {
    // #2
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}
  1. 使用父类ClassPathBeanDefinitionScanner#doScan获取包下BeanDefinition:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
   Assert.notEmpty(basePackages, "At least one base package must be specified");
   Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
   for (String basePackage : basePackages) {
      // 通过给定的包扫描classes下面所有符合的mapper接口
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
         ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
         candidate.setScope(scopeMetadata.getScopeName());
         String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
         if (candidate instanceof AbstractBeanDefinition) {
            postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
         }
         if (candidate instanceof AnnotatedBeanDefinition) {
            AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
         }
         if (checkCandidate(beanName, candidate)) {
            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
            definitionHolder =
                  AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            beanDefinitions.add(definitionHolder);
            registerBeanDefinition(definitionHolder, this.registry);
         }
      }
   }
   return beanDefinitions;
}

2. 对获得的beanDefinitions进行处理:

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (GenericBeanDefinition) holder.getBeanDefinition();
    String beanClassName = definition.getBeanClassName();
    LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
        + "' mapperInterface");

    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    // #1
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
    // #2
    definition.setBeanClass(this.mapperFactoryBeanClass);
    // #3
    definition.getPropertyValues().add("addToConfig", this.addToConfig);

    boolean explicitFactoryUsed = false;
    if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
      definition.getPropertyValues().add("sqlSessionFactory",
          new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionFactory != null) {
      definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
      explicitFactoryUsed = true;
    }

    if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
      if (explicitFactoryUsed) {
        LOGGER.warn(
            () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate",
          new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionTemplate != null) {
      if (explicitFactoryUsed) {
        LOGGER.warn(
            () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
      explicitFactoryUsed = true;
    }

    if (!explicitFactoryUsed) {
      LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
    definition.setLazyInit(lazyInitialization);
  }
}

a. 把原生的className放在了构造器参数里,方便后面代理找到被代理类 b. 这里的把原来的mapper接口class替换成了class org.mybatis.spring.mapper.MapperFactoryBean c. 增加了个属性“addToConfig”值为true,标记下是否用来加入org.apache.ibatis.session.Configuration: MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的

MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的

MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的

到这里@MapperScan的作用就完成了,根据basePackages扫描指定包下mapper接口并注册为Spring bean,beanclass为MapperFactroyBean(用于动态生成Mapper接口的代理实例)。

MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的

时序图

MybatisPlus两个limit引发的思考(上)——@MapperScan是如何生效的 简单描述下大概步骤:

  1. 在spring容器初始化的时候会在invokeBeanFactoryPostProcessors阶段会加载配置类MybatisPlusConfig
  2. 发现配置类上有@Import(MapperScannerRegistrar.class)进而执行它的registerBeanDefinitions注册了MapperScannerConfigurer
  3. 执行MapperScannerConfigurer调用ClassPathMapperScanner进行扫描并注册包下所有符合的BeanDefinition并对其进行处理,最终注册的bean定义class为MapperFactoryBean。

整个流程都在AbstractApplicationContext中的invokeBeanFactoryPostProcessors中进行,核心类有:

  1. MapperScannerRegistrar:是一种ImportBeanDefinitionRegistrar,注册了MapperScannerConfigurer
  2. MapperScannerConfigurer:是一种BeanFactoryPostProcessor,创建了ClassPathMapperScanner并把自己的属性赋值给了它(会把占位符替换为实际的值)
  3. ClassPathMapperScanner:用户扫描并注册包下所有符合BeanDefinition并对其进行处理。

接下来我们继续看看后续如何使用这些bean definition生成代理实例的。

转载自:https://juejin.cn/post/7243796937198059557
评论
请登录