springboot整合mybatis中mapper加载为bean过程梳理

1 篇文章 0 订阅
1 篇文章 0 订阅

关于mybatis

mybatis是一个数据库持久层框架。通过给其配置数据源,让其管理我们与数据库的链接,并且它让我们的代码和sql语句实现了分离。基本使用方法如下,

public static void main(String[] args) throws IOException {

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources
                                    .getResourceAsReader("mybatisConfig.xml"));
        SqlSession sqlSession = sqlSessionFactory.openSession();

        WordfsMapper mapper = sqlSession.getMapper(WordfsMapper.class);
        List<Map<String, Object>> maps = mapper.selectAll();
        deal(maps);
    }

详细的就先不说了,主要说说大概的步骤。首先配置sqlSessionFactory,然后再通过sqlsessionfactory来获取一个跟数据库的链接session。最后所有的crud都是通过这个session获取到的mapper来完成的。这个mapper是我们定义的接口,里面全是未实现的抽象方法。例如本例中的selectAll。

关于spring

在springboot启动过程中,会默认将主启动类所在及其子包下的所有@Component 作为bean注入到上下文环境中,在项目启动后我们就可以直接使用这些bean了,spring也是一样,只不过需要手动配置xml配置文件。

spring注入bean,我们也都知道是有好几种方法的。例如直接在目标类上加@Component注解,或者在配置类中用@Bean注解注入。但是以上种种情况,我们注入进spring容器上下文中的bean都必须是被实例化的,换句话说,注入的bean起码得是一个对象,不能是一个接口。但是我们的mapper偏偏就是一个接口。那么spring是怎么帮我们注入这个mapper的呢?

spring集成mybatis

spring集成mapper非常简单。直接在mapper接口上添加@Mapper注解,完事!想用的话直接@Autowire注入即可。

非常简单!!但是其中spring在后端其实为我们做了很多。我们都不知道(致敬默默奉献的spring),那么我们就来看一下spring是怎么帮我们把这个接口注入容器中让我们使用的吧

首先看一下依赖。由于我这里是引入的mybatisplus,所以这里就是mybatisplus的依赖。其实mapper注入跟mybatis的依赖几乎一样

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatisplus-spring-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
  • 首先先说说第一种注入方式,就是在主启动类上添加@MapperScan注解。我们可以先叫它包扫描注解。先看看这个注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class) //导入了MapperScannerRegistrar这个类
@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 {}; //包路径

可以看到这个注解是导入了MapperScannerRegistrar这个类的。这个类是做什么的呢?我们就来这个类内部看一看。下面是我截取的该类内部的一段代码。

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  /**
   * {@inheritDoc}
   */
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      //获取MapperScan(包扫描)注解
    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);

    //注入各种属性,代码省略

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

    //懒加载相关,代码省略

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

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

  }

可以看到这个类实现了spring提供的ImportBeanDefinitionRegistrar接口。这个接口的作用就是在被别的类@Import导入后,会调用registerBeanDefinitions这个方法。

可以看到,MapperScannerRegistrar 这个类中,重写的registerBeanDefinitions这个方法获取了MapperScan这个包扫描注解,并且判断不为空后,进入之后的方法。下面的方法其实就是把一个叫做MapperScannerConfigurer的类注册进了spring容器中。

所以我们可以把MapperScannerRegistrar 这个暂且叫做mapper的配置类注册器。

  • MapperScannerConfigurer看名字应该像是mapper扫描的配置类。我们也进去看看。

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
	//各种属性,getter,setter

  /**
   * {@inheritDoc}
   * 
   * @since 1.0.2
   */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    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));
    }
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

 //省略别的方法

}

可以看到,这个类是实现了BeanDefinitionRegistryPostProcessor这个接口的。这个接口是函数式接口。作用是在类注册进spring后,进行一些操作。这个操作就在postProcessBeanDefinitionRegistry方法中。而我们前面知道了,MapperScannerConfigurer这个mapper扫描配置类刚刚才被注册进spring,所以此时必然会执行该方法。

该方法本质其实就是new了一个ClassPathMapperScanner(路径mapper扫描器),然后执行了scan方法。我们看看这个方法。

public int scan(String... basePackages) {
        int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
        this.doScan(basePackages);
        if (this.includeAnnotationConfig) {
            AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
        }

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

可以看到,ClassPathMapperScanner是继承了ClassPathBeanDefinitionScanner类的。我们要看的scan方法,ClassPathMapperScanner并没有重写,用的就是父类的scan方法。可是这里重写了doscan这个方法。所以其实真正的逻辑是下面:

@Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    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 {
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

调用父类doscan后,获取扫描到的的所有BeanDefinitionHolder。进行了处理。

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      String beanClassName = definition.getBeanClassName();
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 
        //这一步很重要,偷梁换柱,把所有bean内部的实例class全部换为了MapperFactoryBean.class
      definition.setBeanClass(this.mapperFactoryBeanClass);

      definition.getPropertyValues().add("addToConfig", this.addToConfig);
	//注入sqlSessionFactory
      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;
      }
//注入sqlSessionTemplate(会顶替sqlSessionFactory)
      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        definition.getPropertyValues().add("sqlSessionTemplate",
            new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }
      definition.setLazyInit(lazyInitialization);
    }

可以看到,大概就做了两件事,1.偷梁换柱,把bean对应的mapper的class类统一换为了MapperFactoryBean.class。2. 注入sqlSessionTemplate

至此,mapper在spring容器中注入过程就全部完毕了。可能有人会有疑问了,我要的是mapper,你给我MapperFactoryBean,这能行吗?系统运行起来不得崩溃吗?

下面就该看一下如何从spring中获取对应的mapper了

spring获取mapper

首先,我们进入MapperFactoryBean这个类看一下。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  private Class<T> mapperInterface;

  private boolean addToConfig = true;

  public MapperFactoryBean() {
    // intentionally empty
  }

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

//省略一部分代码
}

可以看到它是集成了FactoryBean的。这其实就是一个bean的工厂类。只不过目前这个工厂只管mapper。

当我们向spring要mapper时,因为mapper在spring中的实例均为MapperFactoryBean这个工厂,此时就会调用getObject()这个方法。

看看这个方法内部:

  public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
  }

@Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

是不是发现什么了,跟我们原生使用mybatis的逻辑几乎是一样的,获取sqlsession(这里是sqlSessionTemplate),再用这个来getMapper。就获取到我们要的mapper了。而这里的mapperInterface,就是我们注入mapperfactory的时候那一句

definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);

把每一个bean对应的构造方法传入当前bean的全类名。就是当前的mapper。就是这里的mapperInterface。

而这个sqlSessionTemplate也是我们刚才注入mapperfactory的时候,注入的sqlSessionTemplate。

这样,其实就是我们获取mapper的底实现。发现到了底层,其实跟我们原生使用差不了太多。

最后,我看网上很多对mybatis自动注入进spring讲解的不够详细,只说了每一个mapper底层其实是mapperfatory工厂,并没有讲明为什么。我这次算是带着大家从源码级别过了一遍mapper的注入流程啦。应该比较详细了。

其实mapper注入除了启动类的MapperScan注解,还有Mapper注解是比较常用的。其实这个跟这个原理差不多。这边就先讲到这里。有兴趣的大家可以自己去看一下相关的逻辑。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值