您的位置:首页 > 编程语言 > Java开发

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读写数据库全过程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息