关于 MyBatis 是如何被集成进 Spring 的

709 阅读6分钟

一、使用对比

先看看独立版本 Mybatis 如何使用

// 1、
String resource = "mybatis-config.xml";
InputStream inputStream =  Resources.getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

// 2、
SqlSession sqlSession = factory.openSession();

// 3、
XxxMapper mapper = sqlSession.getMapper(XxxMapper.class);
XxxPo po = mapper.getBy(2);

// 4、
sqlSession.close();

引入 mybatis-spring 依赖,集成进 Spring 后,调用就变成

@Autowired
private XxxMapper mapper;

// 省略...

XxxPo po = mapper.getBy(2);

调用精简了不止一点点,很好奇这是如何做到的?接下来看看这个项目源码,大概的思路就是将一些 关键的类 托管在了 spring 容器。

本文基于:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.0</version>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.0</version>
</dependency>

首先,先全局预览下这个包的内容,可以看到总共也就这么几个类,所以我们可以先在战略上藐视它。 image.png

但战术上我们不能松懈。三个重点类已经在图上用红框框圈出,我们围绕着这些展开讲。

二、创建并注入 SqlSessionFactory

首先,在 mybatis-spring 中,第一步同样也需要创建 SqlSessionFactory 实例。这个任务由 SqlSessionFactoryBean 类实现,其职责呢就是返回 SqlSessionFactory 类实例,原理是 Spring 非常重要的扩展点之一 FactoryBean

其他的第三方组件集成进时,也是这个套路。FactoryBean 是十分常见的,它就是用来集成其他的外部框架。后面我们还会看到这个类。

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, 
InitializingBean, ApplicationListener<ApplicationEvent> {
    // ..... 省略
    @Override
    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
            afterPropertiesSet();
        }
        return this.sqlSessionFactory;
    }
}

该类实现了 FactoryBean 接口,getObject() 方法返回的是实际需要被 Spring 托管的 Bean 实例。在这里就是 SqlSessionFactory,而不是 SqlSessionFactoryBean 自己。在应用启动时 Spring 判断到注入类实现了 FactoryBean 接口,就会调用 getObject() 方法,将该方法的返回值注册进 IOC 容器。详细见 官网描述

再来看看 SqlSessionFactoryBean 中的 sqlSessionFactory 属性,是在 buildSqlSessionFactory() 方法中根据 XML、Java Config 中定义的属性 new 出来的,如下:

// XML Config 配置
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
</bean>

// Java Config 配置
@Configuration
public class MyBatisConfig {
  @Bean
  public SqlSessionFactory sqlSessionFactory() {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource());
    return factoryBean.getObject();
  }
}

三、创建并注入 Mapper 实例

注入完 SqlSessionFactoryBean 后,接着就要获取 Mapper 实例了。来看看 mybatis-spring 是如何把 Mapper 实例托管给 Spring 容器的。

有以下几种方式能够将 Mapper 实例注册进 Spring,详见 官网描述

  • 在 XML 中配置 MapperFactoryBean
  • 使用 @Bean 标签
  • 在 XML 文件中配置 mybatis:scan
  • 使用 @MapperScan 注解扫描
  • 配置 MapperScannerConfigurer 类

但不管是配置 XML、还是 Java Config 只是入口不同,达到的效果是一样的。这里只讲 Java Config 方式

@Configuration
@MapperScan("org.mybatis.spring.sample.mapper")
public class AppConfig {
  // ...
}

MapperScannerRegistrar 类是 @MapperScan 注解对应的处理器,registerBeanDefinitions 方法根据注解上的属性构造了一个 ClassPathMapperScanner 类并调用了 ClassPathMapperScanner#doScan,最终调用了 ClassPathMapperScanner#processBeanDefinitions 方法,迭代该包下面的所有类,并创建 GenericBeanDefinition

// MapperScannerRegistrar
void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry) {
  ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  // 省略
  scanner.doScan(StringUtils.toStringArray(basePackages));
}

// ClassPathMapperScanner
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;
  }

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
      // 省略
      definition.setBeanClass(this.mapperFactoryBean.getClass());
  }

重点关注,这里使用 MapperFactoryBean 类型去创建 GenericBeanDefinition(与 SqlSessionFactoryBean 一样实现了 FactoryBean 接口)。

definition.setBeanClass(this.mapperFactoryBean.getClass());

在 Spring 在启动时,根据 @MapperScan 中配置扫描的路径,把这个包下面的类都包装为 GenericBeanDefinition,提供给 SpringIOC 容器进行注册,当发现当前这个 BeanClass 实现了 FactoryBean 接口,会调用 getObject() 将其返回值注入容器。(最终会调用到 FactoryBeanRegistrySupport#doGetObjectFromFactoryBean 方法)

详细看下 ClassPathMapperScanner#processBeanDefinitions 方法,使用 Java Config 形式注入时,这两个判断都不会进去

if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
    // ....
} 
else if (this.sqlSessionFactory != null) {
    // ....
}

往下走,最后几行。此时 explicitFactoryUsed 值还是 false,进入了设置 AutowireModeAUTOWIRE_BY_TYPE 逻辑。

if (!explicitFactoryUsed) {
    // ...
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}

这个操作的意思是,能通过 set 方法注入在 Spring 中托管的对应类型的 Bean 实例MapperFactoryBean 类又继承了 SqlSessionDaoSupport,所以有 setSqlSessionFactory 方法,就能拿到 SqlSessionFactory 实例。

使用 setter 方式注入时,Spring 会保证在调用 setSqlSessionFactory 方法前初始化完成 SqlSessionFactory 实例。具体见 官网描述

回到 MapperFactoryBean#getObject 方法,其中 getSqlSession 拿到的是 SqlSessionTemplate 实例,

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

SqlSessionTemplate#getMapper 也是调用了 Configuration#getMapper 方法。所以在客户端使用 @Autowired 注解拿到的实际上就是 myabtis 中托管的 Mapper Proxy 实例。

MapperFactoryBean 中还有一个重要的方法

@Override
protected void checkDaoConfig() {
    // 省略
    configuration.addMapper(this.mapperInterface);
}

这一步是创建 mybatis 核心的 Mapper Proxy 实例,因为 调用端最终还是会去 Configuration 中拿 Mapper 接口实例

到这里,我们就能知道没有 Spring 时,调用方需要在运行时,根据 Mapper 接口类型手动获取到 sqlSession.getMapper(XxxMapper.class) 在 Mybatis 中托管的 Proxy 实例,而 mybatis-spring 项目就是把这个运行时过程转成了启动时的绑定操作。

四、SqlSession 增强并与 Mapper Proxy 关联

稍微回顾下,mybatis 中核心类和 mybaits-spring 项目的类对应关系

MyabtisMybatis-Spring
SqlSessionFactorySqlSessionFactoryBean
Mapper 实例MapperFactoryBean

那 SqlSession 对应的是什么呢?是的,没错,就是最后一个关键类 SqlSessionTemplate。先来看看 SqlSessionTemplate 是怎么被创建出来的

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSessionTemplate sqlSessionTemplate;

  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }
  // ....
}

SqlSessionDaoSupport#setSqlSessionFactory 被 Spring 调用时,就创建了一个 SqlSessionTemplate。换句话说,一个 MapperFactoryBean 实例对应了一个 SqlSessionTemplate 实例

注!在 Spring 中像 RedisTemplateRabbitTemplate 都是单例,这里的 SqlSessionTemplate 却是 多实例。为啥要这么命名,这点存疑。

SqlSessionTemplate 底层构造方法如下:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {
    // 省略
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
      SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class },
      new SqlSessionInterceptor());
}

SqlSessionTemplate 类实现了 SqlSession 接口,就可以和 mybatis 相融合。同时为了拦截并增强 SqlSession 功能,又在构造函数中给成员变量 SqlSession 创建了一个 SqlSessionInterceptor 动态代理。

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

这里就是 mybaits 的事务与 spring 的事务相结合实现的关键。接着用创建好的增强型 SqlSession 实现了如下方法: image.png

相当于每次调用这些方法,都会经过 SqlSessionInterceptor 的拦截。接着看

@Override
public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
}

getMapper 方法传入了 this,把 mybatis 内部的 Mapper Proxy 实例和增强型的 SqlSession(SqlSessionTemplate)结合了起来。

五、流程总结

  1. Spring 启动时通过 SqlSessionFactoryBeanMapperFactoryBean 创建了 Mybatis 核心类 SqlSessionFactoryConfiguration 并注入。将 Mapper 接口类型与实际 MapperProxy 实例做好关联,使用 Autowired 可以访问到。

  2. 程序运行,在调用 Mapper 接口方法时,实际访问的是 MapperProxy 实例方法。此时就会被增强后 SqlSession 也就是 SqlSessionTemplate 中的 SqlSessionInterceptor 拦截到,在这里进行了与 Spring 结合的事务控制。

六、参考