mybatis源码分析7 - mybatis-spring读写数据库全过程

4人阅读 评论(0) 收藏 举报
分类:

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>
  • 1
  • 2
  • 3
  • 4
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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

有了SqlSessionTemplate这个线程安全的sqlSession实现类后,我们就可以将sqlSession创建交给容器来处理了,不需要每次数据库操作都openSession()和close()了。然后和使用原生mybatis一样,可以使用sqlSession的select等直接CRUD方法,或者mapper方式,进行数据库读写了。这儿就有两个问题了

  1. 容器是如何创建SqlSessionTemplate对象的?
  2. 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());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

我们看第三个构造方法即可,首先进行参数校验,确保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);
}
  • 1
  • 2
  • 3
  • 4

容器创建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);
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

invoke方法的步骤主要为

  1. getSqlSession()方法获取sqlSession实例,如何保证sqlSession线程安全的,也是隐含的这个方法中,我们后面会详细分析。
  2. 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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");
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

看到registerSynchronization()方法是应该就恍然大悟了吧,原来是利用ThreadLocal这个线程安全类来实现的呀。不同线程下,ThreadLocal会创建不同对象,故对象只在固定线程下可见,也就是我们通常所说的线程作用域。这样就完全保证了不同线程不可能调用同一个对象了,也就是线程安全了。Spring利用ThreadLocal这个大杀器完美解决了sqlSession线程不安全问题了。

3 总结

mybatis-Spring读写数据库,完美解决了两大问题。一是sqlSession对象创建和管理完全交给Spring容器。二是解决了sqlSession线程不安全问题。至于读写数据库,其实还是通过原生mybatis,sqlSession的select update等方法或者mapper方式。由此可见mybatis-spring仅仅是扩展了mybatis的功能,并适配到Spring容器中而已,并没有去侵入mybatis代码并从本质上去改变他的运行方式。这些都是我们设计框架时可以借鉴的。

查看评论

Mybatis快速入门

-
  • 1970年01月01日 08:00

mybatis-spring 源码分析MapperScannerConfigurer

在Spring配置Mybatis的文件中我们可以看到如下代码: [html] view plain copy        bean class="org.mybatis.spr...
  • jackyechina
  • jackyechina
  • 2016-10-28 16:38:25
  • 1385

Spring-Mybatis源码分析

首先给出Spring-mybatis的配置文件 myba...
  • qq_28007533
  • qq_28007533
  • 2017-07-11 17:50:17
  • 299

Spring整合MyBatis(二)源码分析

在Spring配置Mybatis的文件中我们可以看到如下代码: MapperScannerConfigurer,让它扫描特定的包,自动帮我们成批地创建映射器。这样就大大减少了配置的工作...
  • u012291108
  • u012291108
  • 2016-07-18 20:05:40
  • 3267

深入理解MyBatis-Spring中间件

一 背景 1.1 概念 1.2 使用场景 二 应用 2.1 Mybatis-Spring mybatis是比较常用的数据库中间件,我们来看看怎么在spring中使用mybatis,假设有用户表User...
  • fqz_hacker
  • fqz_hacker
  • 2016-12-09 13:12:50
  • 1891

mybatis-spring-1.3.1.jar官方下载

  • 2017年03月17日 09:48
  • 52KB
  • 下载

mybatis 3.2.1和mybatis-spring 1.1.0

  • 2015年07月15日 16:16
  • 1.59MB
  • 下载

mybatis-spring-1.3.0.jar 下载

  • 2016年09月12日 13:35
  • 393KB
  • 下载

MyBatis整合Spring中间件jar包 mybatis-spring-1.3.0.jar

  • 2017年12月04日 17:23
  • 47KB
  • 下载

mybatis的jar包mybatis-3.1.1.jar与mybatis-spring-1.1.1.jar

  • 2014年12月26日 13:30
  • 613KB
  • 下载
    个人资料
    等级:
    访问量: 1187
    积分: 56
    排名: 161万+