mybatis源码分析7 - mybatis-spring读写数据库全过程
2018-01-13 22:00
417 查看
1 引言
mybatis-spring中,我们利用Spring容器注入的方式创建了sqlSessionFactory,从而完成了mybatis的初始化。那么如何来读写数据库呢?最简单的方式是,和mybatis中一样,利用sqlSessionFactory的openSession来创建sqlSession,然后利用它来select或update,或者mapper方式。这种方式每次都需要手动openSession创建sqlSession对象,和Spring将对象创建和管理交给容器的理念不相符。那么有同学肯定就会说,直接用Spring容器注入sqlSession不就行了吗。但是很不幸,sqlSession是线程不安全的。那么我们该如何做呢?Spring给出了完美的解决方案,sqlSessionTemplete,一个线程安全的SqlSession实现类。使用它的例子如下<!--Spring配置文件中声明SqlSessionTemplate--> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean>
public class UserDaoImpl implements UserDao { // sqlSession由Spring容器注入 private SqlSession sqlSession; public void setSqlSession(SqlSession sqlSession) { this.sqlSession = sqlSession; } // 使用sqlSession的selectOne方法操作数据库 public User getUser(String userId) { return (User) sqlSession.selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId); } }
有了SqlSessionTemplate这个线程安全的sqlSession实现类后,我们就可以将sqlSession创建交给容器来处理了,不需要每次数据库操作都openSession()和close()了。然后和使用原生mybatis一样,可以使用sqlSession的select等直接CRUD方法,或者mapper方式,进行数据库读写了。这儿就有两个问题了
容器是如何创建SqlSessionTemplate对象的?
SqlSessionTemplate是如何解决线程不安全问题的?
带着这两个问题,我们一步步来揭开Spring容器中读写数据库的全过程。
2 Spring容器读写数据库流程
我们先来看第一个问题,容器是如何创建SqlSessionTemplate对象的。2.1 容器创建SqlSessionTemplate对象的过程
来看SqlSessionTemplate的构造方法。// spring bean注入时构造方法 public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); } public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator( sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true)); } // 最终调用的构造方法 public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { // 属性校验,XML属性配置中必须传入sqlSessionFactory notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; // 这儿是关键,创建sqlSession动态代理。 // SqlSessionTemplate的几乎所有操作,如select update delete都是通过这个代理完成的 // 故最终还是调用的sqlSession的select update delete等方法 // 方法调用是,触发InvocationHandler,也就是这儿的SqlSessionInterceptor的invoke方法 this.sqlSessionProxy = (SqlSession) newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); }
我们看第三个构造方法即可,首先进行参数校验,确保sqlSessionFactory等属性已经构造好了,然后创建Proxy动态代理,传入的InvocationHandler是SqlSessionInterceptor,之后sqlSessionTemplete的方法调用,如select update delete,都会由SqlSessionInterceptor的invoke方法完成。不清楚动态代理的同学建议好好复习下Java反射。
sqlSessionTemplete的selectOne update等方法均通过sqlSessionProxy代理完成。如下
public <T> T selectOne(String statement) { // sqlSessionTemplete的select update等方法都是通过sqlSessionProxy代理完成的。 return this.sqlSessionProxy.selectOne(statement); }
容器创建sqlSessionTemplete对象其实很简单,最关键的一点是创建了sqlSessionProxy动态代理,其数据库操作均是通过这个动态代理完成的。接下来我们详细分析这个动态代理的运行过程,以及它是如何保证线程安全的。
2.2 SqlSessionTemplate线程安全地操作数据库
接着上面说,sqlSessionTemplete对数据库的操作,都是通过代理模式,由sqlSessionProxy完成的。而sqlSessionProxy是一个动态代理,其方法调用,都是通过回调内部的InvocationHandler的invoke方法完成的。我们创建sqlSessionProxy动态代理时,传入的InvocationHandler是SqlSessionInterceptor对象,故select update等数据库操作,都是经过它的invoke方法完成的,我们下面详细分析。private class SqlSessionInterceptor implements InvocationHandler { @Override // 动态代理执行方法调用时,回调InvocationHandler的invoke方法. // 故使用sqlSessionTemplete执行数据库select insert等操作时,从invoke方法进入 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 获取sqlSession实例,底层处理了线程同步问题,故SqlSessionTemplete是线程安全的 // 从sessionHolder中获取,或者通过sqlSessionFactory的openSession()方法创建。 // 这就是为什么SqlSessionTemplete是线程安全的原因所在了,这儿也充分体现了mybatis-spring的设计精妙 SqlSession sqlSession = getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { // 方法的反射调用,第一个入参为调用者对象,第二个参数为入参列表。不清楚的同学复习下Java反射 // 故其实就是调用sqlSession对象的method方法,入参为args。 Object result = method.invoke(sqlSession, args); // 如果不是由事务来管理,则强制sqlSession commit一次,因为有些数据库在close前必须commit if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { sqlSession.commit(true); } return result; } catch (Throwable t) { // 异常处理,关闭sqlSession 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); } } } }
invoke方法的步骤主要为
getSqlSession()方法获取sqlSession实例,如何保证sqlSession线程安全的,也是隐含的这个方法中,我们后面会详细分析。
method.invoke(), 执行方法调用。如select update等方法。这是Java反射的方法调用通用方式。
我们下面来详细分析getSqlSession()方法。
// 获取sqlSession实例,它保证了线程安全 public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); // 先从同步事务管理器的 ThreadLocal map中,取出sqlSessionFactory对应的SqlSessionHolder,它是sqlSession的包装类 // 同一线程的同一SqlSessionFactory,才对应同一个sqlSession对象 // ThreadLocal的存在保证了sqlSession的线程安全 SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); // 从sessionHolder缓存中取出SqlSession对象,获取到后就可以将sqlSession返回了。 SqlSession session = sessionHolder(executorType, holder); if (session != null) { return session; } // 没有获取到session时,创建sqlSession实例,还是通过sqlSessionFactory的openSession()方法 LOGGER.debug(() -> "Creating a new SqlSession"); session = sessionFactory.openSession(executorType); // 将构造好的sqlSession封装到SqlSessionHolder缓存中,然后添加到同步事务管理器的threadLocal队列中 // 不同线程下ThreadLocal有不同的实例,这是由于它的存在,保证了sqlSession是线程安全的 registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; }
getSqlSession先从事务管理器的map中取出当前sqlSessionFactory对应的SqlSessionHolder,它是sqlSession的包装类。然后从sessionHolder缓存中取出SqlSession对象并返回。如果map中取不到SqlSession(比如之前根本就没有创建过),那么就需要先openSession来创建SqlSession了。然后将sqlSession添加到事务管理器的map中。这个过程理解起来其实不复杂,简单来说就是缓存命中则从缓存中取,未命中则创建并写入到缓存中。创建sqlSession还是使用的mybatis原生方法openSession(), 那么是如何保证线程安全的呢。关键就在registerSessionHolder()方法中。
// 构造SqlSessionHolder,并将它添加到事务管理器的threadLocal队列中,然后注册并开启事务同步功能,这样才能保证获取sqlSession时是线程安全的了 private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { SqlSessionHolder holder; // 开启了事务同步功能时 if (TransactionSynchronizationManager.isSynchronizationActive()) { Environment environment = sessionFactory.getConfiguration().getEnvironment(); if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]"); // 构造SqlSessionHolder缓存对象,并添加到事务管理器的map中,之后每次从map中取,而不用再创建sqlSession了 holder = new SqlSessionHolder(session, executorType, exceptionTranslator); TransactionSynchronizationManager.bindResource(sessionFactory, holder); // 注册事务同步,TransactionSynchronizationManager管理了一个TransactionSynchronization队列, // 它是一个ThreadLocal,不同线程下有不同的实例,这就是解决线程安全问题的关键所在, // 代码为 ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations"); TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); // 开启事务同步功能 holder.setSynchronizedWithTransaction(true); // 加锁 holder.requested(); } else { if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) { LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional"); } else { throw new TransientDataAccessResourceException( "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization"); } } } else { LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because synchronization is not active"); }
registerSessionHolder()方法先构造SqlSessionHolder缓存对象,并添加到事务管理器的map中缓存起来。然后创建SqlSessionSynchronization对象并添加到事务管理器TransactionSynchronizationManager中(这儿是线程安全的关键所在)。最后开启事务同步功能并申请锁。
我们来看registerSynchronization()方法
// synchronizations是一个ThreadLocal,每个线程下都有不同的实例 private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations"); public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException { Assert.notNull(synchronization, "TransactionSynchronization must not be null"); if (!isSynchronizationActive()) { throw new IllegalStateException("Transaction synchronization is not active"); } else { // 从ThreadLocal中获取本线程下的synchronizations队列,然后将新创建的对象添加进去 ((Set)synchronizations.get()).add(synchronization); } }
看到registerSynchronization()方法是应该就恍然大悟了吧,原来是利用ThreadLocal这个线程安全类来实现的呀。不同线程下,ThreadLocal会创建不同对象,故对象只在固定线程下可见,也就是我们通常所说的线程作用域。这样就完全保证了不同线程不可能调用同一个对象了,也就是线程安全了。Spring利用ThreadLocal这个大杀器完美解决了sqlSession线程不安全问题了。
3 总结
mybatis-Spring读写数据库,完美解决了两大问题。一是sqlSession对象创建和管理完全交给Spring容器。二是解决了sqlSession线程不安全问题。至于读写数据库,其实还是通过原生mybatis,sqlSession的select update等方法或者mapper方式。由此可见mybatis-spring仅仅是扩展了mybatis的功能,并适配到Spring容器中而已,并没有去侵入mybatis代码并从本质上去改变他的运行方式。这些都是我们设计框架时可以借鉴的。相关文章
mybatis源码分析1 - 框架mybatis源码分析2 - SqlSessionFactory的创建
mybatis源码分析3 - sqlSession的创建
mybatis源码分析4 - sqlSession读写数据库完全解析
mybatis源码分析5 - mapper读写数据库完全解析
mybatis源码分析6 - mybatis-spring容器初始化
mybatis源码分析7 - mybatis-spring读写数据库全过程
相关文章推荐
- Spring+MyBatis实现数据库读写分离方案
- Spring+MyBatis实现数据库读写分离方案
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- mybatis源码分析5 - mapper读写数据库完全解析
- mybatis源码分析4 - sqlSession读写数据库完全解析
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring+MyBatis实现数据库读写分离方案
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring+MyBatis实现数据库读写分离方案
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析
- Mybatis源码分析之Spring与Mybatis整合MapperScannerConfigurer处理过程源码分析
- spring+mybatis利用interceptor(plugin)兑现数据库读写分离
- Spring+MyBatis实现数据库读写分离方案
- Spring与Mybatis整合的MapperScannerConfigurer处理过程源码分析/mybatis中文官方文档
- Spring+MyBatis实现数据库读写分离方案