SpringBoot怎么扫描非主类下面的Mapper接口定义(mapperInterface)

首先看上面 我上传了源码哦 !!

首先简单介绍在SpringBoot中如何配置MyBatis自动注入的:
配置文件的配置:

# mybatis的xml文件路径
mybatis.mapper-locations=classpath*:sqlmapper/**/*.xml

需要引入依赖如下

<dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.1.1</version>
 </dependency>

引入这个依赖其实就引入了一下这个依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
</dependency>

当然其中最主要的还是后三者,mybatis与mybatis-spring这两个包就不多说了,mybatis-spring-boot-autoconfigure是非常重要的一个角色,配置了Spring中整合MyBatis需要的Bean实现。
在这里插入图片描述
自动配置无非就两件事情:

  1. 去MyBatis的xml文件目录下扫描xml文件(这个可以通过在配置文件中加入mybatis.mapper-locations来定义)
  2. 去定义了mapperInterface接口的包路径下扫描接口定义(这个需要MapperScannerConfigurer来实现)
    最后根据这些xml文件生成mapperInterface接口定义的代理实现,这样就OK了。

回想在Spring项目中,我们针对MyBatis的配置如下:

<!-- simplest possible SqlSessionFactory configuration -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <!-- Directly specify the location of the MyBatis mapper xml file. This
         is NOT required when using MapperScannerConfigurer or
         MapperFactoryBean; they will load the xml automatically if it is
         in the same classpath location as the DAO interface. Rather than
         directly referencing the xml files, the 'configLocation' property
         could also be used to specify the location of a MyBatis config
         file. This config file could, in turn, contain &ltmapper&gt
         elements that point to the correct mapper xml files.
     -->
    <property name="mapperLocations" value="classpath:/org/mybatis/spring/batch/dao/*.xml" />
</bean>
<!--这个bean可以注入多个-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
    <!-- optional unless there are multiple session factories defined -->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

可能就会发现MapperScannerConfigurer中有个basePackage,但是SpringBoot中没有这个定义呀?那么怎么指定去哪里指定MyBatis的mapperInterface的路径呢?
其实SpringBoot耍了个滑头。SpringBoot默认只将在SpringBoot主类目录下包含@Mapper注解的接口当做MyBatis的mapperInterface。
可参考本人的另一个博客:
https://blog.csdn.net/m0_37607945/article/details/104930971

那么现在有个需求,需要扫描非SpringBoot主类的mapperInterface,而且xml文件与原来不同的路径怎么办?
比如现在主类在com.example.durid.demo目录下,
然后想扫描com.example.mybatis.demo目录下的mapperInterface,
而且xml文件也与原来不是同一个路径(当然可以是其他jar包,甚至网络文件)
在这里插入图片描述

上面这里othermapper下的其实应该是DemoMapper.xml,应该尽量保证与接口名一致,由于非实际项目,就不纠结去改了!

正常逻辑是首先需要增加xml扫描路径,这个简单,mybatis.mapper-locations这个配置本就支持多个路径,那么扫描mapperInterface呢?如果对mybatis-spring熟悉的话,就知道
MapperScannerConfigurer可以定义多个。
直接增加一个配置类如下:

package com.example.durid.demo.config;

import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;

@Configuration
public class OtherMapperScannerConfig {
	@Bean
	public static MapperScannerConfigurer otherMapperScanner() {
		MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer();
		scannerConfigurer.setBasePackage("com.example.mybatis.demo");
		scannerConfigurer.setAnnotationClass(Repository.class);
		return scannerConfigurer;
	}
}

这样是否就可以了呢?启动程序就会发现这个时候原来默认的mapperInterface没法扫描了?为啥呢?查看MybatisAutoConfiguration源码就会发现:默认情况下MapperScannerConfigurer这个类是在AutoConfiguredMapperScannerRegistrar中进行Bean注册的,而AutoConfiguredMapperScannerRegistrar这个类又是在MapperScannerRegistrarNotFoundConfiguration中进行@Import的,而后者(AutoConfiguredMapperScannerRegistrar)能起作用又是在不存在MapperFactoryBean或MapperScannerConfigurer类型Bean的情况下。

  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }
  }

那么此时该怎么办呢?没有自己定义的MapperScannerConfigurer没法扫描非默认的mapperInterface,要是有自己定义的,默认的又没法起作用。那是不是可以自己去进行@Import呢?修改配置类

@Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
public class OtherMapperScannerConfig {

	@Bean
	public static MyMapperScannerConfigurer otherMapperScanner() {
		MyMapperScannerConfigurer scannerConfigurer = new MyMapperScannerConfigurer();
		scannerConfigurer.setBasePackage("com.example.mybatis.demo");
		scannerConfigurer.setAnnotationClass(Repository.class);
		return scannerConfigurer;
	}
}

此时情况与上面相同,但原因不一样,主要是由于Bean定义的先后顺序导致的,通常情况下,SpringBoot先是扫描自己项目下的所有Bean定义元数据进行Bean的定义,然后去扫描第三方的。此时AutoConfigurationPackages.has(this.beanFactory)判断为false.此时AutoConfigurationPackage注解还未起作用。(此处不详细探讨了)
在这里插入图片描述
上面这种方式不行呢,那么需要去调整Bean定义的顺序吗?首先这样比较麻烦(其实本人此处研究也不是很深,哈哈),此处也先不探讨。
有个特别简单的方法。

@Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
public class OtherMapperScannerConfig {

	@Bean
	public static MyMapperScannerConfigurer otherMapperScanner() {
		MyMapperScannerConfigurer scannerConfigurer = new MyMapperScannerConfigurer();
		scannerConfigurer.setBasePackage("com.example.mybatis.demo");
		scannerConfigurer.setAnnotationClass(Repository.class);
		return scannerConfigurer;
	}

	public static class MyMapperScannerConfigurer
			implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
		// 完全拷贝org.mybatis.spring.mapper.MapperScannerConfigurer这个类的实现
	}
}

实现了上面的需求。但是这种实现非常的不优雅,不但代码冗余(相同的代码存在两份),而且存在耦合(依赖第三方实现)。
那么SpringBoot或者MyBatis没有考虑这种需求吗?

以下是官方的优雅方式

首先修改配置文件增加xml扫描路径:

mybatis.mapper-locations=classpath*:sqlmapper/**/*.xml,classpath*:othermapper/**/*.xml

修改主类,通过MapperScans注解进行配置需要扫描的路径

@SpringBootApplication
@MapperScans({ @MapperScan(basePackages = { "com.example.mybatis.demo.mapper" }, annotationClass = Repository.class),
		@MapperScan(basePackages = { "com.example.durid.demo.mapper" }, annotationClass = Mapper.class) })
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

访问地址:http://localhost:8083/actuator/beans
其中关于MapperScannerRegistrar的Bean有如下:

"com.example.durid.demo.DemoApplication#MapperScannerRegistrar#0": {
	"aliases": [],
	"scope": "singleton",
	"type": "org.mybatis.spring.mapper.MapperScannerConfigurer",
	"resource": null,
	"dependencies": []
},
"com.example.durid.demo.DemoApplication#MapperScannerRegistrar#1": {
	"aliases": [],
	"scope": "singleton",
	"type": "org.mybatis.spring.mapper.MapperScannerConfigurer",
	"resource": null,
	"dependencies": []
}

关于两个mapperInterface的定义

"userMapper": {
	"aliases": [],
	"scope": "singleton",
	"type": "com.example.durid.demo.mapper.UserMapper",
	"resource": "file [D:\\20191030\\demo\\target\\classes\\com\\example\\durid\\demo\\mapper\\UserMapper.class]",
	"dependencies": ["sqlSessionFactory", "sqlSessionTemplate"]
},
"demoMapper": {
	"aliases": [],
	"scope": "singleton",
	"type": "com.example.mybatis.demo.mapper.DemoMapper",
	"resource": "file [D:\\20191030\\demo\\target\\classes\\com\\example\\mybatis\\demo\\mapper\\DemoMapper.class]",
	"dependencies": ["sqlSessionFactory", "sqlSessionTemplate"]
},

可以看出此时实现了扫描非主类下面的mapperInterface,查看MapperScan的注解定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {

  /**
   * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
   * {@code @MapperScan("org.my.pkg")} instead of {@code @MapperScan(basePackages = "org.my.pkg"})}.
   *
   * @return base package names
   */
  String[] value() default {};

  /**
   * Base packages to scan for MyBatis interfaces. Note that only interfaces with at least one method will be
   * registered; concrete classes will be ignored.
   *
   * @return base package names for scanning mapper interface
   */
  String[] basePackages() default {};

  /**
   * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
   * package of each class specified will be scanned.
   * <p>
   * Consider creating a special no-op marker class or interface in each package that serves no purpose other than being
   * referenced by this attribute.
   *
   * @return classes that indicate base package for scanning mapper interface
   */
  Class<?>[] basePackageClasses() default {};

  /**
   * The {@link BeanNameGenerator} class to be used for naming detected components within the Spring container.
   *
   * @return the class of {@link BeanNameGenerator}
   */
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  /**
   * This property specifies the annotation that the scanner will search for.
   * <p>
   * The scanner will register all interfaces in the base package that also have the specified annotation.
   * <p>
   * Note this can be combined with markerInterface.
   *
   * @return the annotation that the scanner will search for
   */
  Class<? extends Annotation> annotationClass() default Annotation.class;

  /**
   * This property specifies the parent that the scanner will search for.
   * <p>
   * The scanner will register all interfaces in the base package that also have the specified interface class as a
   * parent.
   * <p>
   * Note this can be combined with annotationClass.
   *
   * @return the parent that the scanner will search for
   */
  Class<?> markerInterface() default Class.class;

  /**
   * Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
   * Usually this is only needed when you have more than one datasource.
   *
   * @return the bean name of {@code SqlSessionTemplate}
   */
  String sqlSessionTemplateRef() default "";

  /**
   * Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
   * Usually this is only needed when you have more than one datasource.
   *
   * @return the bean name of {@code SqlSessionFactory}
   */
  String sqlSessionFactoryRef() default "";

  /**
   * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
   *
   * @return the class of {@code MapperFactoryBean}
   */
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

  /**
   * Whether enable lazy initialization of mapper bean.
   *
   * <p>
   * Default is {@code false}.
   * </p>
   * 
   * @return set {@code true} to enable lazy initialization
   * @since 2.0.2
   */
  String lazyInitialization() default "";
}

在这个注解中不但可以定义包路径而且可以定义接口注解,以及其他一些配置,具体看需求。
这个注解功能实现参考类:

org.mybatis.spring.annotation.MapperScannerRegistrar

再参考MapperScans的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.RepeatingRegistrar.class)
public @interface MapperScans {
  MapperScan[] value();
}

不难看出,解决多个MapperScannerRegistrar类型Bean的注入问题是通过

org.mybatis.spring.annotation.MapperScannerRegistrar.RepeatingRegistrar

这个类来实现的,下面是这个类的源码:

package org.mybatis.spring.annotation;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
 * A {@link ImportBeanDefinitionRegistrar} to allow annotation configuration of MyBatis mapper scanning. Using
 * an @Enable annotation allows beans to be registered via @Component configuration, whereas implementing
 * {@code BeanDefinitionRegistryPostProcessor} will work for XML configuration.
 *
 * @author Michael Lanyon
 * @author Eduardo Macarron
 * @author Putthiphong Boonphong
 *
 * @see MapperFactoryBean
 * @see ClassPathMapperScanner
 * @since 1.2.0
 */
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  /**
   * {@inheritDoc}
   * 
   * @deprecated Since 2.0.2, this method not used never.
   */
  @Override
  @Deprecated
  public void setResourceLoader(ResourceLoader resourceLoader) {
    // NOP
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
    }
  }

  void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);

    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()));

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

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

    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }

  private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
    return importingClassMetadata.getClassName() + "#" + MapperScannerRegistrar.class.getSimpleName() + "#" + index;
  }

  /**
   * A {@link MapperScannerRegistrar} for {@link MapperScans}.
   * 
   * @since 2.0.0
   */
  static class RepeatingRegistrar extends MapperScannerRegistrar {
    /**
     * {@inheritDoc}
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      // 获取MapperScans注解
      AnnotationAttributes mapperScansAttrs = AnnotationAttributes
          .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScans.class.getName()));
      if (mapperScansAttrs != null) {
        // 遍历MapperScans注解进行MapperScanner解析 并注入MapperScannerRegistrar类的Bean
        AnnotationAttributes[] annotations = mapperScansAttrs.getAnnotationArray("value");
        for (int i = 0; i < annotations.length; i++) {
          registerBeanDefinitions(annotations[i], registry, generateBaseBeanName(importingClassMetadata, i));
        }
      }
    }
  }
}

上面这个注解解决多个Bean注入的方式非常优雅,可以从中学习到以下几点:

  1. @Repeatable可重复注解的使用
  2. @Import进行Bean的定义(非常简单获取注解元信息和registry)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值