Mybatis整合Spring的事务控制与SqlSession线程安全问题

在Spring与Mybatis框架整合中,主要有两个重要改动,分别是事务与SqlSession。mybatis-spring包中为以上两个问题提供了解决方案。

  • 重要组件
  1. SpringManagedTransaction (Spring事务管理器)
  2. SqlSessionTemplate (SqlSession的实现)
  3. SqlSessionFactoryBean(整合中取代SqlSessionFactoryBuilder作用,创建SqlSessionFactory类)
  4. SpringManagedTransactionFactory(Spring事务工厂)
  • 整合流程

(1)在MybatisAutoConfiguration中给容器中注入了一个SqlSessionFactory 。

 @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);

而这个SqlSessionFactory 中的SpringManagedTransactionFactory,这个类实现了mybatis的顶级接口TransactionFactory,此时mybatis将不再管理事务,连接的获取和回滚也完全由Spring管理。

        targetConfiguration.setEnvironment(new Environment(this.environment, (TransactionFactory)(this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory), this.dataSource));

这样的话每次在mapper执行相关方法时都会创建一个事务对象,这里自然就是创建的Spring的事务对象。

public class SpringManagedTransaction implements Transaction {
    private static final Logger LOGGER = LoggerFactory.getLogger(SpringManagedTransaction.class);
    private final DataSource dataSource;
    private Connection connection;
    private boolean isConnectionTransactional;
    private boolean autoCommit;

    public SpringManagedTransaction(DataSource dataSource) {
        Assert.notNull(dataSource, "No DataSource specified");
        this.dataSource = dataSource;
    }

    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            this.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);
        LOGGER.debug(() -> {
            return "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring";
        });
    }

    public void commit() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
            LOGGER.debug(() -> {
                return "Committing JDBC Connection [" + this.connection + "]";
            });
            this.connection.commit();
        }

    }

    public void rollback() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
            LOGGER.debug(() -> {
                return "Rolling back JDBC Connection [" + this.connection + "]";
            });
            this.connection.rollback();
        }

    }

    public void close() throws SQLException {
        DataSourceUtils.releaseConnection(this.connection, this.dataSource);
    }

    public Integer getTimeout() throws SQLException {
        ConnectionHolder holder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
        return holder != null && holder.hasTimeout() ? holder.getTimeToLiveInSeconds() : null;
    }
}

在openConnection的时候,事务对象是从DataSourceUtils.getConnection(this.dataSource);此时Spring会往当前线程中存一个Connection连接,那么在同一个方法中,多个mapper在获取connection时,其实都是获取的一个connection,当整个service方法执行结束时,Spring的事务管理器会统一执行commit方法。

(2)MybatisAutoConfiguration中同时给容器中注入了一个SqlSessionTemplate。

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }

SqlSessionTemplate 替代了之前的sqlsession,用来解决mapper之间的线程安全问题。
SqlSessionTemplate的getMapper代码如下:

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

可以看到,此时依然是调用的configuration对象的getMapper方法,但是SqlSession对象传递的是this,而这里的this为sqlSessionTemplate对象,无论是通过mapper还是sqlId,在最终执行sql时,都是用的sqlSession对象。那么我们就看SqlSessionTemplate对象是如何执行sql的。

以selectList方法为例,我们发现sqlSessionTemplate中的相关方法都是通过sqlSessionProxy对象去执行的。

    public <E> List<E> selectList(String statement) {
        return this.sqlSessionProxy.selectList(statement);
    }

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.sqlSessionProxy.selectList(statement, parameter);
    }

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        return this.sqlSessionProxy.selectList(statement, parameter, rowBounds);
    }

而通过构造方法排查,这个sqlSessionProxy竟然是个SqlSession的代理对象。

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
        Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        Assert.notNull(executorType, "Property 'executorType' is required");
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
    }

代理对象的实现逻辑封装在了SqlSessionTemplate.SqlSessionInterceptor类中,我们只需要弄清这个类的实现原理即可。

果不其然这个类实现了InvocationHandler接口

  private class SqlSessionInterceptor implements InvocationHandler {
        private SqlSessionInterceptor() {
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

            Object unwrapped;
            try {
                Object result = method.invoke(sqlSession, args);
                if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }

                unwrapped = result;
            } catch (Throwable var11) {
                unwrapped = ExceptionUtil.unwrapThrowable(var11);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }

                throw (Throwable)unwrapped;
            } finally {
                if (sqlSession != null) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }

            }

            return unwrapped;
        }
    }

在invoke方法中是通过 SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
来获取sqlSession的,我们跟进这个方法。

在这里插入图片描述
发现最终还是调用了openSession这个方法,但是这里要注意了,每次调用任何mapper的任何方法,都会重新开启一个openSession,每个session中的事务管理器也都是spring管理,这样的话就避免了线程安全问题,保证每一个mapper方法的调用都会有独立的sqlSession,同时事务又交由Spring管理。这样Mybatis可以与Spring进行整合,实现面向接口编程。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值