MyBatis原理(五)——SpringBoot整合MyBatis

一、创建SqlSessionTemplate与SqlSessionFactory

我们知道spring-boot-starter是通过加载spring.factories文件里的配置类来自动注入的。mybatis-spring-boot-starter下的mybatis-spring-boot-autoconfigure下的META-INF/spring.factories里配置了MybatisAutoConfiguration。

  • MybatisAutoConfigurationsqlSessionFactory方法注入了SqlSessionFactoryBean,通过SqlSessionFactoryBean的getObject方法完成了Configuration的解析与SqlSessionFactory(DefaultSqlSessionFactory)的创建。
  • MybatisAutoConfigurationsqlSessionTemplate方法通过上面创建的sqlSessionFactory来完成SqlSessionTemplate的创建。

SqlSessionTemplate包含DefaultSqlSessionFactory包含Configuration。

SqlSessionTemplate是在MyBatis自带的DefaultSqlSession的基础上增加了对sqlSession的管理,帮助创建和缓存sqlSession,具体看SqlSessionInterceptor。

二、创建MapperProxy

我们用的Mapper其实是经过jdk动态代理后的MapperProxy,创建流程如下:

  1. 我们通常在主类里用@MapperScan注解来加载mapper。SpringBoot容器在invokeBeanFactoryPostProcessors时,其中一个BeanDefinitionRegistryPostProcessor(简称BDRPP)——ConfigurationClassPostProcessor会解析配置类,因为@MapperScan里面有@Import,所以它会解析注解@MapperScan里的@Import(MapperScannerRegistrar.class)。然后执行MapperScannerRegistrar的registerBeanDefinitions方法,注册MapperScannerConfigurer的BD,并传入mapper的路径basePackage。
  2. 还是在invokeBeanFactoryPostProcessors方法里。执行MapperScannerConfigurer的postProcessBeanDefinitionRegistry(因为它也是BDRPP)。创建扫描器ClassPathMapperScanner(继承自ClassPathBeanDefinitionScanner),执行scan方法,它的doScan先调用父类的doScan方法,加载basePackage下的mapper.class文件,创建BD,然后执行processBeanDefinitions方法:设置BD的构造方法参数为mapper的类名(这里是字符串,但spring在设置参数时会根据类型自动转为对应的Class);设置BD的beanClass为MapperFactoryBean;如果@MapperScan没有指定sqlSessionTemplateRef和sqlSessionFactoryRef的话,设置BD的AutowireMode为AUTOWIRE_BY_TYPE。以上就完成了MapperFactoryBean的BD的注册。
  3. 到了创建bean的时候,上面说MapperFactoryBean的BD设置了AUTOWIRE_BY_TYPE,根据spring的自动注入,MapperFactoryBean的setSqlSessionFactory与setSqlSessionTemplate就会被调用,进行注入。MapperFactoryBean类图:
    MapperFactoryBean.png

    sqlSessionTemplate就在SqlSessionDaoSupport里。他实现了InitializingBean,所以完成创建之后执行DaoSupport的afterPropertiesSet方法:
// DaoSupport
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
    // Let abstract subclasses check their configuration.
  // 看MapperFactoryBean的checkDaoConfig实现
    checkDaoConfig();

    // Let concrete implementations initialize themselves.
    try {
    // 空方法
        initDao();
    }
    catch (Exception ex) {
        throw new BeanInitializationException("Initialization of DAO failed", ex);
    }
}

// MapperFactoryBean的checkDaoConfig实现
protected void checkDaoConfig() {
  // SqlSessionDaoSupport的实现里里检查sqlSessionTemplate是否存在
  super.checkDaoConfig();
  // 检查mapperInterface
  notNull(this.mapperInterface, "Property 'mapperInterface' is required");
  // 取出Configuration
  Configuration configuration = getSqlSession().getConfiguration();
  if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
    try {
      // 添加mapper
      // 里面是调用mapperRegistry.addMapper(type);
      // 创建MapperProxyFactory,放到一个HashMap里,并解析Mapper
      configuration.addMapper(this.mapperInterface);
    } catch (Exception e) {
      logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
      throw new IllegalArgumentException(e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

初始化完了,接着是getObject():

// MapperFactoryBean
public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

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

// Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

// MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 取出前面创建的MapperProxyFactory
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 实例化
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

// MapperProxyFactory,用jdk动态代理创建MapperProxy
public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

三、MapperProxy原理

MapperProxy实现了InvocationHandler,我们看invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 一般不成立
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
      // 一般是创建PlainMethodInvoker
      // 然后invoke里面mapperMethod.execute(sqlSession, args);
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

// 根据method找到MapperMethodInvoker并缓存
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      // 如果是default方法,那就直接用该方法,一般不走这个逻辑
      if (m.isDefault()) {
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return new DefaultMethodInvoker(getMethodHandleJava9(method));
          }
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException
            | NoSuchMethodException e) {
          throw new RuntimeException(e);
        }
      } else {
        // 否则创建一个MapperMethod,用PlainMethodInvoker包装。
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

可以看到最终是调用的MapperMethod的execute方法。再点进去看,它里面的会根据方法的类型,返回结果类型,调用对应的SqlSession的增删改查方法,这里也就是SqlSessionTemplate的增删改查方法。而SqlSessionTemplate里的增删改查方法又是用它的sqlSessionProxy干的,那这个sqlSessionProxy是个啥玩意呢?我们先看一下SqlSessionTemplate的构造方法:

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) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  // jdk动态代理
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

就是jdk动态代理出一个SqlSession,看SqlSessionInterceptor:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取DefaultSqlSession,里面有ThreadLocal逻辑,具体看下面第三节
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      // DefaultSqlSession的方法
      Object result = method.invoke(sqlSession, args);
      // 没开启spring事务,那就提交事务
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
    } 
    // 省略其他代码...
  }
}

三、事务

如果开启了spring事务,那么Service会被AOP代理增强,增强的逻辑在实现了MethodInterceptor的TransactionInterceptor里。
执行它的invoke方法,里面调了父类的invokeWithinTransaction方法。
如果是声名式事务:

  • 创建事务上下文
    • AbstractPlatformTransactionManager.doBegin()开始事务:通过数据源获取连接,封装成ConnectionHolder;用连接开启事务;然后以数据源为key,ConnectionHolder为value存到TransactionSynchronizationManager(简称tsm)的ThreadLocal<Map<Object, Object>>(简称tl)里。
    • AbstractPlatformTransactionManager.prepareSynchronization()设置tsm的一些属性,表示开始了事务
  • invocation.proceedWithInvocation()执行service方法,里面执行MapperProxy的方法。
  • 出现异常的话,completeTransactionAfterThrowing()如果配了对应的事务异常,那就回滚,否则提交
  • cleanupTransactionInfo()清理事务的相关ThreadLocal里的信息
  • commitTransactionAfterReturning() 提交事务

问题来了,连接被保存到tsm的tl里了,那mybatis执行的时候是如何取到这个连接的呢?
回顾一下,mybatis里执行的时候,获取连接是在BaseExecutor的getConnection方法里,里面调用transaction.getConnection(),
这个transaction其实是SpringManegedTransaction,是在创建SqlSessionFactory时设置的,即SqlSessionFactoryBean.buildSqlSessionFactory()里面的:

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

SpringManegedTransaction.getConnection方法调用openConnection()方法,里面调用DataSourceUtils.getConnection方法,
再调doGetConnection方法,可以看到是用数据源从tsm的tl里拿出连接,也就是设置事务的那个连接。

关于SqlSession
还记得在SqlSessionInterceptor的invoke方法里,如果没开启事务,那么就执行sqlSession的提交:

if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
  // force commit even on non-dirty sessions because some databases require
  // a commit/rollback before calling close()
  sqlSession.commit(true);
}
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  return (holder != null) && (holder.getSqlSession() == session);
}

isSqlSessionTransactional从tsm里用sessionFactory获取sqlSessionHolder,这个是在啥时候塞进去的?
看getSqlSession方法:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
  // 用sessionFactory从tsm的tl里取出SqlSessionHolder
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  SqlSession session = sessionHolder(executorType, holder);
  // 如果有就返回
  if (session != null) {
    return session;
  }
  // 否则创建sqlSession,然后registerSessionHolder
  LOGGER.debug(() -> "Creating a new SqlSession");
  session = sessionFactory.openSession(executorType);
  // 判断是否开启spring事务,是的话创建SqlSessionHolder塞到tsm的tl里。
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

registerSessionHolder方法先调用tsm的isSynchronizationActive方法判断是否开启事务,因为AbstractPlatformTransactionManager.prepareSynchronization()时初始化过tsm,所以这里判断出开启事务。接着就创建SqlSessionHolder,跟SqlSessionFactory一并塞到tsm的tl里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容