Mybatis-Spring整合和事务

Scroll Down

概述

Spring与Mybatis我认为应该有两方面:
1、mybatis在Spring容器中是如何构建的。
2、mybatis的事务与Spring的事务是如何融合使用的。

本文将讲述Spring与mybatis是如何整合的,以及Spring的事务与mybatis的事务如何融合使用。

首先,我们先看下不在Spring环境下Mybatis是如何使用的。

public void mybatis() { 
    // 1、加载资源
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    // 2、构建SqlSessionFactory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
    // 3、通过SqlSessionFactory构建SqlSession
    SqlSession sqlSession = factory.openSession();
    // 4、通过SqlSession获取用户接口代理类
    UserDao userDao = sqlSession.getMapper(UserDao.class);
    // 5、执行方法
    userDao.addUser(user);
    // 6、提交
    sqlSession.commit();
    // 7、关闭
    sqlSession.close();
}

注意这里面内容还不完整,其实还需要考虑事务。后续会说明,
那么上面这段代码中使用mybatis的步骤应用到Spring中应该有如下几点:
1、构建SqlSessionFactory。
2、构建SqlSession。
3、根据SqlSession获取用户的接口代理类。
4、用户调用对应的方法。
5、用户提交commit(事务下会自动提交)
6、关闭SqlSession。

经过上述5个步骤,mybatis的全部流程就已经走完了。

那么回到我们的目标上来,上述代码在Spring环境中应该是什么样的呢?

首先,构建SqlSessionFactory可以使用@Configuration注解将SqlSessionFactory注入到Spring容器中,Spring中使用的是SqlSessionFactoryBean。

SqlSessionFactoryBean实现了FactoryBean,那么在创建对象的时候会触发回调函数getObject(),返回实际的对象,下面我们来看下这个方法的实现。

@Override
  public SqlSessionFactory getObject() throws Exception {
    // 如果sqlSessionFactory为空,那么调用afterPropertiesSet方法进行初始化。实际上不会走到这步,Spring的初始化比执行getObject回调更早。
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }
  
   @Override
  public void afterPropertiesSet() throws Exception {
    // 构建SqlSessionFactory之前需要数据源。
    notNull(dataSource, "Property 'dataSource' is required");
    // sqlSessionFactoryBuilder这个对象一定有
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");
    // 执行构建SqlSessionFactory逻辑
    this.sqlSessionFactory = buildSqlSessionFactory();
  }
  
  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      // 通过配置文件中的configLocation参数获取文件的位置
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      // 获取配置类。
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }
    // 设置objectFactory,支持用户自定义objectFactory实现
    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }

    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
        }
      }
    }

    if (!isEmpty(this.typeAliases)) {
      for (Class<?> typeAlias : this.typeAliases) {
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type alias: '" + typeAlias + "'");
        }
      }
    }
    // 支持用户配置Plugin
    if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }
    // 用户自定义TypeHandler
    if (hasLength(this.typeHandlersPackage)) {
      String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeHandlersPackageArray) {
        configuration.getTypeHandlerRegistry().register(packageToScan);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
        }
      }
    }
    // 将上面解析到的TypeHandler注册到Configuration中
    if (!isEmpty(this.typeHandlers)) {
      for (TypeHandler<?> typeHandler : this.typeHandlers) {
        configuration.getTypeHandlerRegistry().register(typeHandler);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type handler: '" + typeHandler + "'");
        }
      }
    }
    
    if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
      try {
        configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }
    // 支持用户自定义缓存实现
    if (this.cache != null) {
      configuration.addCache(this.cache);
    }

    if (xmlConfigBuilder != null) {
      try {
        // // 执行构建Mybatis配置类逻辑,其中会加载Mybatis运行中所有用到的数据结构。
        xmlConfigBuilder.parse();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    // 设置默认的事务工厂类,后面事务会用到
    if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }
    // 将事务工厂,数据源设置到环境变量中
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    // 解析Mapper文件
    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }
    // 到此,SqlSessionFactory所需要全部参数都执行完了,上面所有的步骤其实就是我们上述的步骤1
    return this.sqlSessionFactoryBuilder.build(configuration);
  }

到此,SqlSessionFactory所需要全部参数都执行完了,上面所有的步骤其实就是我们上述的步骤1。
下面我们继续讲步骤2是怎么来的。

在Spring中使用MapperFactoryBean来完成用户对象的构建的,后来的扫描DAO的功能实际上对应的类也是这个类,这个类也实现了FactoryBean。那么Spring同样也会在创建这个对象时回调getObject返回真正的对象。方法如下:

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

上面的方法做了两件事。
1、获取SqlSession。
2、通过SqlSession获取对应的Mapper代理类。
可以看到,这两步对应着我们上面说的步骤2和步骤3。

下面我们先看。方法如下:
构建每个MapperFactoryBean的时候,都会首先注册一个SqlSessionFactory。可以通过XML依赖注入,也可以通过扫描方式的时候手动注入。
MapperFactoryBean的构造器需要一个用户定义的DAO接口,后续会通过这个接口实现代理。

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
     // 这里会通过入参的SqlSessionFactory构建SqlSessionTemple也就是SqlSession
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

到此为止,我们已经获取到了SqlSession,以及通过SqlSession获取用户的代理类。

下面我们来看下SqlSessionTemplate实现的逻辑。

  // SqlSessionFactory
  private final SqlSessionFactory sqlSessionFactory;
  // 用户可以指定,没有指定就是默认的实现。
  private final ExecutorType executorType;
  // 这里做了一层代理,目的我们后续讲
  private final SqlSession sqlSessionProxy;
  
  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;
    // 通过动态代理,将SqlSession类的方法做了增强。
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
  
  private class SqlSessionInterceptor implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 通过SqlSessionFactory获取SqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
         // 执行SqlSession对应的方法
        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()
          // 自动commit
          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) {
          // 最后关闭SqlSession
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

用户在每次执行数据库操作的方法的时候都会先创建一个SqlSession,然后执行数据库操作,最后关闭SqlSession。
这里我们单独把这个getMapper方法拿出来看下:

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

这个方法里面,首先获取Configuration,然后通过Configuration的getMapper方法获取代理类,getMapper方法有两个参数,第一个是接口的类型,第二个参数是在执行数据库操作的时候的SqlSession。这里是SqlSessionTemple的实例。这里我们可以把SqlSessionTemplate理解成一个创建SqlSession的一个代理类,实际操作数据库的可不是这个SqlSessionTemplate,而是拦截器里面新生成的SqlSession。

这里暂时先记一个问题就是我们现在可以通过SqlSession既可以创建一个Mapper代理类,也可以通过SqlSession来执行数据库操作。这个其实是不符合接口的单一职责的,并且这里面容易产生费解的地方在于,前者可以直接先new一个SqlSessionTemplate,通过这个SqlSessionTemplate的功能来创建一个代理类,当然这个代理类引用的真实对象还是SqlSessionTemplate,但是SqlSessionTemplate里面的其他操作数据库的动态又做了一层增强,就是创建一个SqlSession,这个SqlSession才是真正执行数据库操作的那个类。

注意:SqlSessionTemplate使用代理模式将update\query等数据库操作都通过sqlSessionProxy代理类调用了,也就是说在指定上述方法的时候都会执行一下上述的模板方法。

至此,我们其实已经分析完了Spring下Mybatis的集成以及SqlSession的指定逻辑了。

在分析Spring和Mybatis事务交互的机制之前,我们先看下原生的Mybatis事务是如何实现的。
mybatis原生的事务都需要用户去手动操作SqlSession.commit/rollback,最后会映射到Executor里面的Transaction.commit/rollback
1、我们正常创建一个SqlSession的时候一般都是带有数据源的。那么就会执行下面这个方法。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    // 代表一个事务
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // 获取事务的工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 获取真正的事务,这个事务包含一个数据库连接池
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

Mybatis中有几种事务的实现。
1、JdbcTransaction:该实现类实现了commit/rollback/openConnection等方法。
2、ManagedTransaction:该实现类只是实现了openConnection方法,commit/rollback方法都是空实现。
3、SpringManagedTransaction:该实现类实现了从Spring的事务管理器中获取连接。而commit/rollback方法都不会被真正调用。

下面我们再来看下Mybatis和Spring的事务是如何交互的。

首先我们来看Spring的事务的是如何工作的。我们应该先有一个概念,就是事务和数据库连接是一对一的,也就是一个事务就对应一条数据库的连接,这个事务结束了,这个数据库连接也就被释放到数据库连接池中去了。
1、识别方法级别的注解@Transactional。
2、对标记这个注解的方法进行拦截增强,实现事务的相关流程,获取一个连接,开启事务,执行,提交/回滚。

那么mybatis里面的事务动作其实和Spring里面的动作是一样的。
接下来我们看接口定义:

public interface Transaction {

  /**
   * Retrieve inner database connection
   * @return DataBase connection
   * @throws SQLException
   */
  Connection getConnection() throws SQLException;

  /**
   * Commit inner database connection.
   * @throws SQLException
   */
  void commit() throws SQLException;

  /**
   * Rollback inner database connection.
   * @throws SQLException
   */
  void rollback() throws SQLException;

  /**
   * Close inner database connection.
   * @throws SQLException
   */
  void close() throws SQLException;

  /**
   * Get transaction timeout if set
   * @throws SQLException
   */
  Integer getTimeout() throws SQLException;
  
}

这里需要注意的是上面讲解的SqlSessionInterceptor中调用的getSqlSession方法。

 public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
    // 首先尝试通过SqlSessionFactory获取SqlSessionHolder。
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    // 获取对应的SqlSession
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }
    // 如果上面没有获取到那么这里会新建一个SqlSession并将其放入到事务管理器中。然后这里会继续调用Spring的SpringManagedTransaction。
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

SpringManagedTransaction中获取一个连接的实现如下:

public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    // 调用Spring的事务管理器根据DataSource获取连接。保证Spring事务中管理的连接和Mybatis中使用的连接是同一个连接
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }

调用Spring的事务管理器根据DataSource获取连接。保证Spring事务中管理的连接和Mybatis中使用的连接是同一个连接。

要想将sql语句的执行由mybatis执行, 事务的提交或者回滚操作由Spring控制, 两者需要关联使用同一个connection, 在不同的方法中调用connection的相关方法操作, (所以, Spring并没有直接使用mybatis sqlSession中提供的提交或者回滚方法) . 如何安全的获取同一个connection?这就需要使用TransactionSynchronizationManager。
Spring 没有直接使用 MyBatis 的 Transaction 中的事务管理的 begin/commit/rollback 方法,而是通过 SpringManagedTransaction 类中持有的 java.sql.Connection 对象直接进行事务管理的。
JDBC 连接的生命周期分为: 连接的创建、提交、回滚和关闭
我们将连接的创建/关闭分为一组,叫连接的管理,将连接的 提交/回滚 分为一组,叫事务的管理。
MyBatis 原生的 连接管理 和 事务管理 是交给 org.apache.ibatis.transaction.Transaction 来管理的。
Spring-tx 主要封装的是事务管理,事务管理操作是通过 DataSourceTransactionManager 来实现的。而连接的管理是通过 org.springframework.jdbc.datasource.DataSourceUtils 来操作具体的 DataSource 来实现的。

Spring与Mybatis的整合主要关注两个类,一个是SqlSessionFactoryBean,另一个是SqlSessionTemplate。

  • SqlSessionFactoryBean:主要用于构建SqlSessionFactory。
  • SqlSessionTemplate:可以理解成SqlSession的Spring层的代理类。
  • MapperFactoryBean:用于获取用户自定义DAO接口实现类。Spring中该类持有的SqlSession为SqlSessionTemplate。

Springboot中的AutoConfigure相关类也是在构建的时候使用了这两个类。

Mybatis中获取连接的方式有两种:
一种是内置的的Transaction,与Spring整合的时候用的是SpringManagedTransaction。
另一种是直接使用Connection

Connection之于SqlSession是必须的。

Transaction支持数据源创建和指定的连接创建。

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

我们看到,在获取SqlSession的时候,可以通过Connection获取一个SqlSession,还可以通过默认的方式,默认的方式需要从数据库连接池。
这里猜测其实是Mybatis在设计的时候就需要考虑面向一个三方使用者已经通过其他方式从数据源等获取连接了,比如Spring。还需要考虑如果Mybatis自己管理一个数据源。但是在于Spring整合的过程中并没有直接传入一个Connection的方式获取一个SqlSession,而是通过默认的方式从数据源中获取,这里与Spring的部分进行了重写,是从事务管理器中获取数据库连接。为什么要这样呢?
这样做的原因是想根据DataSourceUtil从事务管理器通过数据源获取同一个连接,实现事务。这里的数据源获取连接的方法其实应该放在SqlSessionTemplate里面去先去获取,不再依赖Mybatis的Environment获取数据源,因为Environment中的数据源有可能被篡改导致数据源前后不一致,进而导致数据库连接不一致。

1、Spring与mybatis整合的时候,应该怎么整合。。。
2、Spring的事务和Mybatis的事务是怎么融合的。。。

不要记流水账,不要看这里对,看那里也对,要有整体的思想。这个整体思想是
1、要达成什么样的目的,要解决什么问题。
2、达成这个目的需要有哪些必要条件

要总结出一个方法论