聊一聊Mybatis-Spring都帮我们做了什么

MyBatis-Spring 会帮助我们将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,本文将跟随Mybatis-Spring 的源码,一步步探究是它是如何实现的。

我们先进行一个必要的bean配置。

@Configuration
@MapperScan(basePackages = { "com.xxx.goods.dao" }, sqlSessionFactoryRef = "goodsSqlSessionFactory")
public class GoodsDataSourceConfig {
    @Resource
    private Environment env;

    @Bean(name = "goodsDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.goods")
    public DataSource initDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "goodsTransactionManager")
    @Primary
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "goodsSqlSessionFactory")
    @Primary
    public SqlSessionFactory initSqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(env.getProperty("mybatis.goods.mapper-locations")));
        return sessionFactory.getObject();
    }
}

Mybatis原生写法

我这里引用Mybatis官方文档中的说明

  • SqlSessionFactory SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
  • SqlSession 每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}
  • Mapper 映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:
try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

}

我们分析一下Mybatis-Spring如何做到的

Mybatis配置时我们使用了@MapperScan注解,底层会使用MapperScannerConfigurer进行扫描并将Mapper注册到Spring容器中,核心代码要看org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions这个方法。

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

    // 这两句代码时关键,这里将 beanClass 进行了重新设置,设置为了 MapperFactoryBean,并且将原来接口 class 设置为构造方法参数
    definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
    definition.setBeanClass(this.mapperFactoryBean.getClass());

    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,如果有配置
    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;
    }

    // 如果 sqlSessionFactory & sqlSessionTemplate 都没有指定,那么启用类型自动注入
    if (!explicitFactoryUsed) {
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}

方法中this.mapperFactoryBean.getClass()会固定返回org.mybatis.spring.mapper.MapperFactoryBeandefinition.getBeanClassName()返回的Mapper的类名作为MapperFactoryBean的属性。
Spring初始化进行装配时会调用MapperFactoryBeangetObject()

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

看到这,就知道了怎么将Mapper声明成bean,但是getSqlSession()返回的是属性中的sqlSession,Mybatis原生的写法中sqlSession不是线程安全的,那Mybatis-Spring如何做到用一个sqlSession处理多个线程的请求?我们继续看这个sqlSession有什么特别的。

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  if (!this.externalSqlSession) {
    this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
  }
}

此处使用的sqlSession并非是Mybatis的DefaultSqlSession,而是Mybatis-Spring提供的SqlSessionTemplate


public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
      SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class },
      new SqlSessionInterceptor());
}

@Override
public <T> T selectOne(String statement) {
  return this.sqlSessionProxy.<T> selectOne(statement);
}

SqlSessionTemplate虽然是SqlSession的实现,但内部真正调用并非是他自己,而是生成了一个代理类,我们再看一下代理。

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

sqlSessionProxy运行时,每次都会判断当前事务中是否有SqlSession,没有就重新创建,这样可以保证多线程访问不会出现问题,也可以保证事务的传递。

总结

Mybatis-Spring代码量不大,但实现比较巧妙,如果对Mapper注入和Spring事务感兴趣可以深入了解一下。

参考文档

MyBatis.org

mybatis-spring

mybatis MapperScannerConfigurer 源码剖析

Mybatis源码系列3-三种SqlSession的区别


聊一聊Mybatis-Spring都帮我们做了什么
https://blog.yjll.blog/post/112d5fa0.html
作者
简斋
发布于
2020年11月3日
许可协议