Mybatis——Spring事务实现

前面学习Mybatis中Executor执行sql时,只研究了非事务执行时,直接DataSource.getConnection()创建连接。这里学习一下事务执行。

前提:SpringAOP——事务实现中学习到:Spring开启事务时会创建一个“事务连接”(需要手动提交的连接)然后通过ThreadLocal的方式,存储到当前线程中。

起因是面试官问了一个问题:spring中的事务连接和mybatis中的事务连接是否是同一个?

回答说:是同一个,但是说不出具体实现来(ThreadLocal)。

也就是说“事务连接”是Spring中创建的,Mybatis执行sql时,会从线程中取出“事务连接”,然后创建statement执行。结合前面学习的mybatis源码,用源码求证一下

 一、复习mybatis与spring的交界处——代理实现

 MapperFactoryBean.getObject()创建了一个接口的代理实例,InvocationHandler是MapperProxy类型,方法调用代码如下:

/* org.apache.ibatis.binding.MapperProxy#invoke */
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        //Object的方法直接执行mapperProxy的方法
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        //静态方法直接执行
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //new MapperMethod(mapperInterface, method, configuration)
    //实际new sqlCommond(mapperInterface, mehod, configuration)
    //sqlCommond.name = mapperStatement.getId
    //sqlCommond.type = ms.getSqlCommondType()--insert|delete|update|select
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //sqlSession根据sqlCommondType执行对应sql,例如当sqlCommondType == select时
    //sqlsession.select(sqlCommond.name)
    return mapperMethod.execute(sqlSession, args);
  }

/* org.apache.ibatis.binding.MapperMethod#execute */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

二、mybatis中事务实现

以上面的insert为例,result = rowCountResult(sqlSession.insert(command.getName(), param));

sqlSession.insert()-->BaseExecutor.update()-->SimpleExecutor.doUpdate()

/* org.apache.ibatis.executor.SimpleExecutor#doUpdate */
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      //这里获取连接并创建statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //获取连接
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

/* org.apache.ibatis.executor.BaseExecutor#getConnection */
  protected Connection getConnection(Log statementLog) throws SQLException {
    //获取连接transaction.getconnection()
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

重点是BaseExecutor初始化时,transaction属性的类型。

/* org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource */
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //transaction属性
      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();
    }
  }

  private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    //需要确定environment.getTransactionFactory的类型
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    return environment.getTransactionFactory();
  }

确定environment.transactionFactory属性:SpringManagedTransactionFactory类型的实例

/* org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory */
  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    //...

    if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

    //transactionFactory是SpringManagedTransactionFactory类型实例
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    //...

    return this.sqlSessionFactoryBuilder.build(configuration);
  }

所以transaction是SpringManagedTransaction类型实例

/* org.mybatis.spring.transaction.SpringManagedTransactionFactory#newTransaction(javax.sql.DataSource, org.apache.ibatis.session.TransactionIsolationLevel, boolean) */
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
  }

Mybatis事务最终获取连接:SpringManagedTransaction.getConnection()

/* org.mybatis.spring.transaction.SpringManagedTransaction#getConnection */
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    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");
    }
  }
/* org.springframework.jdbc.datasource.DataSourceUtils#getConnection */
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            return doGetConnection(dataSource);
        }
        catch (SQLException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
        }
        catch (IllegalStateException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage());
        }
    }

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
        //从当前线程中获取连接
        //connectionHolder = Thread.currentThread().get("Transactinal resources").get(dataSource)
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                //不是事务连接时,从DataSource获取一个连接
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            //事务执行时,从Thread中获取spring创建的事务连接
            //非事务执行时,直接从Datasource获取一个连接
            return conHolder.getConnection();
        }
        // Else we either got no holder or an empty thread-bound holder here.
        //获得一个没有事务连接的空封装器ConnectionHolder时,从DataSource获取一个连接
        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = fetchConnection(dataSource);

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            try {
                // Use same Connection for further JDBC actions within the transaction.
                // Thread-bound object will get removed by synchronization at transaction completion.
                ConnectionHolder holderToUse = conHolder;
                if (holderToUse == null) {
                    holderToUse = new ConnectionHolder(con);
                }
                else {
                    holderToUse.setConnection(con);
                }
                holderToUse.requested();
                TransactionSynchronizationManager.registerSynchronization(
                        new ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }
            }
            catch (RuntimeException ex) {
                // Unexpected exception from external delegation call -> close Connection and rethrow.
                releaseConnection(con, dataSource);
                throw ex;
            }
        }

        return con;
    }

验证完毕:

“事务连接”是Spring调用DataSource创建的,并放到线程中,Mybatis执行sql时,会从线程中取出“事务连接”,然后创建statement执行。(ThreadLocal实现)

“非事务连接”由mybatis直接调用DataSource创建。

补充:mybatis的两种config文件,需要注意传统配置文件,配置可能导致spring事务失效。

① 依赖mybatis-spring.jar后的配置文件,默认初始化SQLSessionFactoryBean时,初始化SpringManagedTransactionFactory放入到Environment中

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="mapper/UserMapper.xml"/>
    </bean>

② 传统的配置文件,<environment />有个属性<transactionManager />与上面transactionFactory对应,需要设置成SpringManagedTransactionFactory.class,否则Spring事务无效

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 配置文件的根元素 -->
<configuration>
        <!-- ... -->
    <!-- 环境:配置mybatis的环境 -->
    <environments default="dev">
        <!-- 环境变量:可以配置多个环境变量,比如使用多数据源时,就需要配置多个环境变量 -->
        <environment id="dev">
            <!-- 事务管理器 -->
            <!-- "JDBC" == JdbcTransactionFactory.class 直接从datasource获取连接,会导致spring事务失效-->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 数据源 -->
            <dataSource type="POOLED">
                <property name="driver" value="${mysql.driver}"/>
                <property name="url" value="${mysql.url}"/>
                <property name="username" value="${mysql.username}"/>
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!-- ... -->
</configuration>

 

posted on 2020-03-22 16:18  FFStayF  阅读(865)  评论(0编辑  收藏  举报