深入剖析 mybatis 原理(三)如何整合 Spring

本贴最后更新于 2532 天前,其中的信息可能已经时移世易

# 前言

在前两篇文章我们在 mybatis 源码中探究了他的运行原理,但在实际使用中,我们需要将其和 Spring 整合使用,特别是当下流行的 SpringBoot,那么,myBatis 在 SpringBoot 中是如何运行的呢?

我们需要带着问题去研究,这样才能印象更深刻,以下是楼主的问题,不知道各位有没有自己的问题,如果有,也可以和楼主一起探讨,或者自己查看源码。

  1. SqlSessionFactory,SqlSession 如何生成?
  2. Mapper 代理如何生成?如何运行?

1. SqlSessionFactory,SqlSession 如何生成?

由于楼主的项目是 SpringBoot ,因此基本没有配置文件,只有一个简单的配置,这也是 Spring 团队一直追求的目标:无配置。但由于我们的团队开始使用 SpringCloud ,于是配置又多了起来,看来,配置文件始终是消灭不掉的。那么,废话了这么多,楼主的关于 Mybatis 的配置由以下几个部分组成:

  1. jar 包 maven 导入 artifactId 为 mybatis-spring 的 jar 包,该 jar 包是整合 Spring 和 mybatis 的粘合剂。
  2. 使用硬编码的方式配置 bean。比如 SqlSessionFactory,SqlSessionTemplate, PlatformTransactionManager.
  3. 扫描接口包。

配置 SqlSessionFactory

  @Bean(name = "sqlSessionFactory")
  public SqlSessionFactory sqlSessionFactoryBean() {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource());

    bean.setTypeAliasesPackage(TYPE_ALIASES_PACKAGE);

    // 添加插件
    bean.setPlugins(MybatisUtil.getInterceptor());

    // 添加XML目录
    ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

    bean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
    return bean.getObject();
  }

注意:dataSource 方法会返回一个楼主自己配置的多数据源。但这不是我们今天的重点。

SqlSessionTemplate 配置

  @Bean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }

通过刚刚的 sqlSessionFactory 创建一个 SqlSessionTemplate ,该类非常重要,是 Spring 和 Mybatis 整合的核心。稍后会详细介绍。

PlatformTransactionManager 事务管理配置

  @Bean
  @Override
  public PlatformTransactionManager annotationDrivenTransactionManager() {
    return new DataSourceTransactionManager(dataSource());
  }

注意:为什么这里会有一个重写注解,因为楼主这个类实现了 TransactionManagementConfigurer 接口,该接口是 Spring 的事务基础结构中的核心接口。应用程序可以直接使用它,但他并不作为主要 API,通常,应用程序可以使用 TransactionTemplate 或通过 AOP 进行声明式事务划分。

可以看到,楼主在这三个方法上面都加入了 @Bean 注解,表示这个方法会返回一个 Bean。

还有就是如何扫描包 @MapperScan(basePackages = "cn.think.in.java.mapper"),通过这个注解扫描需要生成代理的包名。

好了,我们开始一个个分析,首先看 SqlSessionFactory 如何生成,我们先看看我们写的代码,首先创建了一个 SqlSessionFactoryBean, 然后设置该 Bean 的数据源,再然后设置别名,再设置插件,再设置资源 URL,最后调用 getObject 方法返回一个 SqlSessionFactory。我们看到了 getObject 方法,如果看过我们 Spring 源码解析系列文章,就知道,这式 Spring 扩展接口 FactoryBean 中一个非常重要的方法,其中有一种类型的 AOP 就是通过该接口实现的。我就不讲这个接口的原理了。那么,这个 SqlSessionFactoryBean 肯定实现了该接口,我们看看源码:

可以看到该类实现类 Spring 中几个重要的接口,比如 FactoryBean ,InitializingBean 接口。这对该类的拓展起到了非常大的作用。我们再看看该类有哪些属性:

可以看到该类俨然一个 SqlSession,该有的属性都有了,资源,配置,数据源,事务工厂,SqlSessionFactoryBuilder, SqlSessionFactory,类型处理器, 别名,缓存,对象工厂,环境。。。。等等等,一应俱全。注意:该类在 org.mybatis.spring 包下,也就是说,该类就是 Spring 和 Mybatis 的整合包,粘合剂。

我们重点看看该类的 getObject 方法:

逻辑很简单,如果 sqlSessionFactory 为 null,则调用 afterPropertiesSet 方法,该方法核心是调用了自身的 buildSqlSessionFactory 方法,我们看看该方法实现。该方法代码很多,楼主就不贴出来了,主要逻辑就是创建 Configuration 对象,然后设置 Configuration 的各种参数,比如插件,别名, 然后创建一个 SpringManagedTransactionFactory 事务工厂赋值给默认属性。再给 configuration 对象设置环境属性,参数是 事务工厂,数据源, 默认环境 “SqlSessionFactoryBean”,然后开始解析设置的 mapperLocations 到 configuration 对象中, 最后调用 sqlSessionFactoryBuilder.build(configuration) 返回一个 SqlSessionFactory,和我们之前创建 SqlSessionFactory 基本相同。返回的也是默认的 DefaultSqlSessionFactory。

那么有了 SqlSessionFactory ,就可以创建 SqlSession 了, 如何创建呢? 还记得我们配置的 SqlSessionTemplate 吗,该类就是 MyBatis 将 Spring 和 MyBatis 框架粘合的类,我们看看该类的继承关联体系图:

可以看到该类实现了 SqlSession 接口,同时也依赖个 SqlSession 的代理. 那么我们再看看该类的属性方法:

属性图

该类含有一个 SqlSessionFactory 工厂类,一个执行器类型(SqlSession 底层使用的 ExecutorType 类型),一个 SqlSessionProxy JDK 生成的代理对象, 还有一个异常转换器。

方法图

我们看到该类有实现了 SqlSession 的所有方法,但内部全部委托了 JDK 代理的 SqlSessionProxy 来实现。 我们还注意到,有一个拦截器内部类,该类就是创建 JDK 动态代理时的那个拦截类, 我们看看该类的 invoke 方法实现:

SqlSessionInterceptor 实现

我们看看方法,首先调用 SqlSessionUtils 的静态方法 getSqlSession 获取 SqlSession 对象, 然后调用 SqlSession 的响应方法, 检测事务是否由 Spring 管理,并根据此结果决定是否提交事务。 最后,返回结果,并在 finally 块中清除 Session(将 Connection 赋值为 null)。

也就是说,在执行 SqlSession 的 SelectOne 之类的方法的时候,都会经过该类。每条 SQL 的事务也都是在这里进行处理(如果 Spring 没有管理的话)。

2. Mapper 代理如何生成?如何运行?

好了,我们有了 DefaultSqlSessionFactory,那么什么适合创建 SqlSessionTemplate 呢,答案是再创建 Mapper 代理的时候,当 Spring 对 Controller 的 bean 进行依赖注入的以后,会循环寻找引用,找到 Service 层,接着找到 Service 层的 Mapper,那么,Mapper 代理怎么来的呢?

还记得我们写过一个注解:@MapperScan(basePackages = "cn.think.in.java.mapper"),这个注解在 IOC 初始化的时候会起到作用。

虽然我们只使用了该注解的一个字段,但我们仔细看看该注解还有没有其他功能:


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

  String[] value() default {};

  // 包名, 这些指定的包都会被扫描
  String[] basePackages() default {};
  // basePackages 的替代方法,扫描指定的类
  Class<?>[] basePackageClasses() default {};
  // 名称生成器
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
  // annotationClass 注解标记的接口
  Class<? extends Annotation> annotationClass() default Annotation.class;
  // 此属性指定扫描程序将要搜索的父项。
  // 扫描器将注册基本包中的所有接口
  // 指定的接口类作为父类。
  // 注意这可以和annotationClass结合使用。
  Class<?> markerInterface() default Class.class;
  // sqlSessionTemplate 的引用
  String sqlSessionTemplateRef() default "";
  // sqlSessionFactory 的引用
  String sqlSessionFactoryRef() default "";
  // 指定一个自定义的MapperFactoryBean来作为spring bean返回一个mybatis代理。
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

}

可以看到该注解功能繁多,那么该注解作用是什么呢?使用这个注解来注册 MyBatis 映射器接口。也就是说,我们在启动类上加入了 @ MapperScan 注解,而该注解同时又含有 @Import 注解,在 IOC 启动的时候,会加载该注解标识的类,也就是 MapperScannerRegistrar.class,该类是什么样子的呢?以下是该类继承图:

Spring IOC 在启动的时候会调用该类的 registerBeanDefinitions 方法,该方法很长,我们还是看看该方法:

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

    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    // this check is needed in Spring 3.1
    if (resourceLoader != null) {
      scanner.setResourceLoader(resourceLoader);
    }

    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
      scanner.setAnnotationClass(annotationClass);
    }

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      scanner.setMarkerInterface(markerInterface);
    }

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

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
    }

    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

    List<String> basePackages = new ArrayList<String>();
    for (String pkg : annoAttrs.getStringArray("value")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    scanner.doScan(StringUtils.toStringArray(basePackages));
  }

首先找到 @MapperScan 注解, 然后一个一个的处理注解中的属性。最后关键的一点执行 ClassPathMapperScanner 的 doscan 方法,参数是包名数组。我们看看该方法实现:

首先调用父类的 doScan 方法,返回了一个 BeanDefinitionHolder 的 Set 集合,然后判断是否为空,不为空则执行 processBeanDefinitions 方法,该方法可谓非常的重要,该方法会将制定包下的 Mapper 接口改成 mapperFactoryBean 的类型,也就是说,Spring getBean 返回的就是 mapperFactoryBean 类型,我们还是看看该方法:

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      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) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

该方法循环处理所包下所有的 Bean 定义对象,首先修改 Bean 的 Class 为 mapperFactoryBean ,然后增加了很多属性,比如 addToConfig, sqlSessionFactory,sqlSessionTemplate,并且将该 Bean 的注入类型设置为按照类型注入。 最后在 doScan 方法中返回了这些修改过的 Bean 定义。

我们有头绪了,捋一捋,首先,我们在启动类上写入了注解,标注哪些类或者哪些包需要扫描,并且该注解包含一个 @Import 注解,Spring 会将该注解标识的类 MapperScannerRegistrar 加入到 IOC 的启动过程,然后执行该类的 registerBeanDefinitions 注册 Bean 定义方法, 在该方法中,会将制定包或接口全部修改,加入一些和 SqlSession 相关的属性,并将该接口的 Bean 的类型改为 mapperFactoryBean 类型。 那么 mapperFactoryBean 是什么呢?

我们看看 MapperFactoryBean 的继承图谱:

MapperFactoryBean

可以看到,该类实现了 Spring 常用的扩展接口 FactoryBean,也等于拥有了 getObject 方法,我们说,可以在该方法中动一些手脚。继承了 SqlSessionDaoSupport, 该类中有几个方法我们能够看到,就是设置 SqlSessionFactory 和 SqlSessionTemplate,getSession 等方法。该类可以说是开发者们在 Dao 层支持类,如果可以,完全可以继承该类,获取 SqlSession 直接操作数据库。但是这样就太复杂了。当然还有,MapperFactorybean 还间接实现了 InitializingBean 接口,也就是 Spring 留给我们的扩展接口。 需要重写 afterPropertiesSet 方法。我们还是看看该类吧。

该类既然是个 FactoryBean ,那么我们第一个看的就是他的 getObject 方法:

是不是很熟悉,该方法就是获取 SqlSession 然后调用 SqlSession 的 getMapper 方法,参数是 mapperInterface 接口(该参数在构造器中传入)。也就是说,Spring 根据 Bean 定义对象,找到该方法,再在 getObject 方法中构造代理对象。我们终于知道了为什么在 ClassPathMapperScanner 的 doScan 方法中要将接口的 Bean 定义的改成 MapperFactorybean ,原来最终的目的就是调用 getObject 方法,然后调用 getMapper 方法。并且设置的那些属性就是在对 MapperFactoryBean 的父类 SqlSessionDaoSupport 的属性进行赋值。

getMapper 方法我想应该不用解释了吧,在我们关于 Mybatis 的第二篇文章中已经知道所有逻辑了。

还记得 MapperFactoryBean 实现的 InitializingBean 接口,该接口定义的方法是在属性设置完毕后执行,那么该方法是如何执行的呢?

首先执行了 checkDaoConfig 方法,该方法是个抽象方法, 然后调用了 initDao 方法,该方法是个空方法。那么 checkDaoConfig 的具体实现是什么呢?

该方法会校验接口是否存在,然后调用 configuration 的 addMapper 方法,该方法底层调用了 MapperRegistry 的 addMapper 方法,将 namespace 属性和 配置文件关联。这之前我们已经分析过了。

我们开始说,创建 SqlSessionTemplate,是在 IOC 初始化 Mapper 的时候创建的。什么时候呢? 就是在 ClassPathMapperScanner 的 processBeanDefinitions 方法中,设置了 sqlSessionTemplate 属性,最终会触发 SqlSessionDaoSupport 的 setSqlSessionTemplate 方法,该方法从容器中获取 SqlSessionTemplate 实例,从而触发我们编写的 new SqlSessionTemplate 方法,如果不写也没事,setSqlSessionFactory 会默认创建一个。

好了,我们可以总结一下 Mapper 的创建过程:

  1. IOC 通过注解扫描指定包名,在初始化的时候调用 @MapperScan 注解中指定的类最终执行 doScan 方法,将所有的 Mapper 接口的 Bean 定义都改成 FactoryBean 的子类 MapperFactoryBean,并将该 SqlSessionFactory 和 SqlSessionTemplate 添加到该类中。

  2. Spring IOC 在实例化该 Bean 的时候,需要传入接口类型,并将 SqlSessionFactory 和 SqlSessionTemplate 注入到该 Bean 中。并调用 configuration 的 addMapper 方法,解析配置文件。

  3. 当调用 MapperFactoryBean 的 getObject 方法的时候,事实上是调用 SqSession 的 getMapper 方法,而这个方法会返回一个动态代理对象。所有对这个对象的方法调用都是底层的 SqlSession 的方法。

而 Spring 和 MyBatis 的整合也和 AOP 相似,都是通过 Spring 留下的扩展接口 FactoryBean 来实现的。在 FactoryBean 中包装了 SqlSession ,而 SqlSession 则会返回代理。

3. 总结

这是我们剖析 mybatis 的第三篇文章了,我们分析了 Mybatis 是如何整合 Spring 的,通过 mybatis 提供的 mybatis-spring 的 jar 包,粘合了 Spring 和 mybaits, mybatis 和 AOP 一样,都是通过扩展 Spring 提供的各种接口来完成扩展功能,比如 Factory,在 SqSessionTemplate 中,底层还是调用 Mybatis 自己的 SqlSession 创建动态代理来实现的。可谓万变不离其宗。同时,我们也学习到了 Java 世界两大框架的优秀,比如灵活扩展。非侵入式,特别式 Spring,设计的可谓趋于完美。作为程序员,我们更要向这些优秀的源码学习。加油!!!

good luck!!!

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 384 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • xinhong

    这是我看到的最好的分析 mybatis 和 Spring 整合的文章。看完后如醍醐灌顶。😄 加油

  • 其他回帖
  • someone

    加油!