SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)

阅读本文需要spring源码知识,和springboot相关源码知识
对于springboot 整合mybatis,以及mybatis源码关系不密切的知识,本文将简单带过

系列文章目录和关于我

一丶从一个问题开始——读已提交情况下mybatis一级缓存造成的问题

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

上图中,已知道users1的size为5,那么users2的大小为多少昵?

我们暂且抛弃mybatis框架中的知识,从mysql事务隔离级别进行分析,test方法第一次查询到总数,然后重新开启一个事务插入了一条(require_new的传播级别),后续addOne方法将立即提交,再次查询的时候,test方法应该可以立马查询到已经提交的数据,应该比第一次输出的应该多1,这是事务隔离级别指导我们做出的判断

但是事实上是users1大小和 users2一样大,这是为什么昵?

我们看下控制台

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

发现mybatis并没有进行第二次数据库的查询,这时候我们应该意识到mybatis具备缓存,从而导致第二次查询并没有访问数据库

也就是说 读已提交的隔离级别下,mybatis如果不关闭缓存将存在错误(这里的缓存指的一级缓存,二级缓存普遍是不开的)

具体原理,笔者此文讲到mybatis缓存后将进行解读,下面我们从springboot 和 mybatis整合,到mybatis执行原理展开讲mybatis的原理

二丶mybatis-springboot-starter的自动装配

通常springboot整合mybatis只需要引入如下依赖

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

简单描述就是SpringBoot启动的时候会读取META-INF/spring.factories中自动配置的类,加入到容器中,后续springboot会将这些类当作配置类进行解析

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

上图是mybatis-spring-starter的META-INF/spring.factories,其中关键的是MybatisAutoConfiguration

1.导入SqlSessionTemplate,SqlSessionFactory

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里可以看到当容器中没有SqlSessionFactory的时候,MybatisAutoConfiguration会为我们注入一个SqlSessionFactorySqlSessionTemplate同样如此。

这里我们简单提一下SqlSessionFactorySqlSessionTemplate的作用

1.1.mybatis中的SqlSessionFactory

故名思意,它是创建SqlSession的工厂

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里Spring构建SqlSessionFactory,使用了SqlSessionFactoryBean#getObject

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

它实现了InitializingBean,但是由于没有被注入到容器中,所以其#afterProperties并不会被spring容器回调,在此方法中会调用buildSqlSessionFactory 进行别名扫描,TypeHandler注册,xml解析(调用XMLMapperBuilder#parse),拦截器注册,并且指定事务工厂使用SpringManagedTransactionFactory(mybatis,spring事务结合的关键,后续详细解析)等工作

那么什么是SqlSession?

1.2.mybatis中的SqlSession

SqlSession是mybatis操作数据库抽象出来的接口,它可以执行增删改查,提交事务,回滚事务,创建mapper。我们平时依赖注入的mapper,其实一个动态代理类,其底层其实是调用SqlSession进行的数据库操作

1.3.mybatis-spring中的SqlSessionTemplate

这个类和上面两个类都不同,它是org.mybatis.spring这个包下面的,一般是mybatis-spring这个依赖会引入的类,它的作用是SqlSession,与Spring事务管理一起工作,以确保实际使用的SqlSession与当前Spring事务关联。此外,它还管理会话生命周期,包括根据Spring事务配置在必要时关闭、提交或回滚会话。

它是spring事务和mybatis事务结合的关键,后面用到了我们再详细唠唠

2.注入AutoConfiguredMapperScannerRegistrar

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里可以看到如果没有MapperFactoryBeanMapperScannerConfigurer这两个bean ,那么会import一个AutoConfiguredMapperScannerRegistrar,我们简单说下这三个类的作用,后续用到了详细解析其原理

2.1MapperFactoryBean

MapperFactoryBean 是一个FactoryBean,FactoryBean中有一个方法叫getObject负责创建一个对象交给spring容器管理,通常我们定义的Controller,Service都具备实现类,而非一个接口,spring可以实例化一个service的实现,但是mybatis中的mapper往往是一个接口,spring不知道如何实例化这个mapper,这时候发现mapper的BeanDefinition中标记了这个class是MapperFactoryBean 就会调用MapperFactoryBean#getObject实例化一个mapper,这个mapper便是我们注入到service中使用的mapper,它是源mapper的动态代理实现类,从而在代理类中调用Sqlsesession执行对应的sql操作
《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.2AutoConfiguredMapperScannerRegistrar

在没有MapperScannerConfigurer,mybatis自动装配会为我们注入它,它是一个ImportBeanDefinitionRegistrar

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

spring解析配置类的时候,若发现一个bean是ImportBeanDefinitionRegistrar的实现,那么会调用其registerBeanDefinitions方法,从而注入其他bean的BeanDefinition,这里bean便是MapperScannerConfigurer(ImportBeanDefinitionRegistrar注入的MapperScannerConfigurer扫描的时候要求mapper标注@Mapper注解)

2.3 MapperScannerConfigurer

MapperScannerConfigurer还可以使用@MapperScan或者@MapperScans注解,进行引入,若我们使用了@MapperScan或者@MapperScans,上面的AutoConfiguredMapperScannerRegistrar将不会被Import,AutoConfiguredMapperScannerRegistrar的作用便是默认配置一个MapperScannerConfigurer是一个BeanDefinitionRegistryPostProcessor

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

spring容器在启动的时候,会回调它的postProcessBeanDefinitionRegistry在这个方法里面会扫描所有的mapper接口,指定其class为MapperFactoryBean,从而在后续的实例化中,调用MapperFactoryBean#getObject生成mapper接口的动态代理对象

三丶mybatis扫描mapper接口,注册mapper的Beanfinition

1.MapperScannerConfigurer是如何被注册到spring容器中的

上文中,我们说到,如果我们没有使用@MapperScan或者@MapperScans注解标注在配置类上面,那么会默认添加一个MapperScannerConfigurer,进行mapper接口的扫描注册工作

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

通常启动类都有这样的@MapperScan

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

@MapperScan上面存在@Import,会导入一个MapperScannerRegistrar,这是一个ImportBeanDefinitionRegistrar会在这里注册MapperScannerConfigurer的bean定义信息

其实就是把@MapperScan注解上的配置,绑定到MapperScannerConfigurer的属性上,

@MapperScan注解,可以指定mapper在的包,mapper接口必须标注的注解,Mapper接口动态代理对象生成使用的MapperFactoryBean

2.MapperScannerConfigurer 如何进行扫描注册mapper的

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

其实扫描注册的工作委托给了ClassPathMapperScanner,调用scan方法进行扫描注册

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

它是一个ClassPathBeanDefinitionScanner的子类,ClassPathBeanDefinitionScanner就是负责包路径扫描,注册BeanDefinition的

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里的扫描调用了ClassPathBeanDefinitionScanner的doScan方法,这个方法会根据包路径解析成Resouce对象,然后根据路径下的类包装成BeanDefinition(ScannedGenericBeanDefinition)

重点看下processBeanDefinitions

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里最关键的是definition.setBeanClass(this.mapperFactoryBeanClass),即将mapper接口的BeanDefinition类型指定为MapperFactoryBean,这样在spring后续实例化mapper的时候就调用MapperFactoryBean#getObject方法进行实例化了

至此我们学习了SpringBoot是如何和mybatis进行结合的,下面总结成一图

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

四丶Mapper bean的实例化

当我们一个Service需要注入一个mapper的时候,会从Spring容器中找对应的实例,这时候边会涉及到这个mapper的实例化,但是我们mapper明明是一个接口呀,如何实例化昵?

虽然我们mapper是一个接口,但是注入到service属性上的是这个接口的实现类,它是mybatis动态代理后生成的对象。

这个实例化的入口便是AbstractBeanFactory#getBean方法

1.获取mapper对应的BeanDefinition

这里获取的beanDefinition便是源自ClassPathMapperScanner 注册到容器中的

2.实例化MapperFactoryBean

我们上面说到过,实例化mapper需要调用MapperFactoryBean#getObject,那么首先需要实例化一个MapperFactoryBean

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里实例化MapperFactoryBean边是使用的createBean方法,然后Spring会使用反射调用构造方法实例化出MapperFactoryBean(Spring还存在使用CGLIB生成子类然后实例化的方式),其中调用的是

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这个构造方法需要一个入参,表示Mapper接口类型,那么这个mapperInterface入参来自那么昵?ClassPathMapperScanner扫描完mapper接口,生成BeanDefinition后,还会在BeanDefinition中记录全限定类型,这个全限定类名将作为MapperFactoryBean的构造器入参

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

3.MapperFactory进行属性注入

上面我们得到一个MapperFactoryBean,但是它构造出一个mapper需要借助SqlSession,这里使用的SqlSession其实是SqlSessionTemplate,我们指导MybatisAutoConfiguration会让容器中注入一个SqlSessionTemplate,那么spring是如何把这个SqlSessionTemplate设置到mapperFactoryBean的属性上的昵?

这一步就发生在populateBean方法中,其会调用applyPropertyValues,它会根据javaBean的内省,获取其需要SqlSessionFactory和SqlSessionTemplate,然后从容器中获取MybatisAutoConfiguration注入的实例,进行反射调用Set方法注入

4.MapperFactory的初始化

MapperFactory的父类SqlSessionDaoSupport继承自DaoSupport(),其中DaoSupport又实现了InitializingBean,在Spring实例化MapperFactory,完成依赖注入后将回调InitializingBean#afterPropertiesSet

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

其中checkDaoConfig方法被MapperFactoryBean重写

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里会调用configuration.addMapper解析xml和mybaits相关的注解,然后进行注册和接口进行绑定,但是这一步解析xml操作通常不会真正进行,因为在创建SqlSessionFactory的时候已经进行了

5.调用MapperFactory#getObject实例化出一个mapper

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

实例化出一个Mapper接口的动态代理对象,调用的是SqlSessesionTemplate#getMapper

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

那么到底mapper方法调用的时是如何操作数据库的昵?这一点我们后面继续说

至此我们知道了我们service注入的mapper其实是mybatis使用动态代理生成的对象,表面是一个什么方法实现都没有的接口,其实是动态代理”负重前行”,下图展示了一个mapper被创造出来的全流程

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

五丶Mybatis 和spring事务的结合

上面我们知道了xxMapper其实是一个jdk动态代理生成的对象 ,其InvocationHandlerMapperProxy

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

当mapper被调用其接口中声明的方法的时候,会调用到InvocationHandler#invoke这时候MapperProxy就会大显身手

1.MapperProxy#invoke

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

MapperProxy内部使用了一个Map缓存方法和对应的执行器(MapperMethodInvoker),这个map通常来自MapperProxyFactory的ConcurrentHashMap属性。而真正方法的调用又委托给了MapperMethod#execute,MapperMethod根据方法调用的类型(增删改查)调用MapperProxy中的属性SqlSession(spring环境下的sqlSession实现类是SqlSessionTemplate)`对应的方法

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.SqlSessionTemplate 与mybatis spring事务

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

SqlSessionTemplate实现了SqlSession接口,但是真正进行数据库操作的时候,都是委托给属性SqlSessionProxySqlSessionTemplate存在的意义在于"模板"——复用SqlSession,那么为什么需要复用,为何要复用?我们接着看下它的构造方法

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

可以看到,其内部的sqlSessionProxy是一个动态代理类,我们看下SqlSessionInterceptor,它是一个InvocationHandler

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.1 mybatis 和 spring结合后即使没有开启事务也能自动提交的原因

上图可以看到如果事务并非交给spring管理(调用mapper执行单条增删改查的数据库操作,会自动提交事务)在反射调用sqlsession方法后,会进行事务提交。

//上面无事务注解 下面这条语句会调用到sqlsession的动态代理对象,进行自动提交
public void test(){
 
 	xxxMapper.insertOne(xx);
 }

笔者校招的时候,面试官问过这个问题,我尼玛扯到了mysql的自动提交

原生mybatis使用sqlsession执行数据库操作后,需要手动调用其commit方法。spring环境的mybatis会自动提交,便是由于sqlsessionTemplate复用的sqlsession,其实是DefaultSqlsession的代理类,在执行数据库操作后,发现事务没有被spring管理便进行自动提交

2.2使用mapper执行多个数据库修改操作,具备事务的原因

@Transactional
public void test(){
   xxxmapper.insert1();
    xxxmapper.insert2();
}

众所周知,上面这个方法spring容器中的bean执行是具备事务的,那么为啥具备事务昵?

你可能会回答,容器中的bean是被BeanPostProcesser在bean完成实例化,依赖注入,后会被BeanPostProcessor后置处理器,依次进行处理,生成代理对象,其中存在AnnotationAwareAspectJAutoProxyCreator(@Aspect,@Before等注解感知能力的BeanPostProcessor,会将@Aspect标记的bean中的方法解析成Adivor然后使用ProxyFactory生成其代理对象)或者说任何AbstractAdvisorAutoProxyCreator,它会将使用Advisor 并基于CGLIB,或者java接口动态代理生成代理对象,其中便有BeanFactoryTransactionAttributeSourceAdvisor(一个Advior,真正事务代理的逻辑在TransactionInterceptor(一个Advise,实现事务开启,事务提交,回滚等逻辑)]中,以及TransactionAttributeSource(用于解析事务注解,判断方法是否需要开启事务,TransactionAttributeSourcePointcut这个pointcut 便是使用它进行判断方法是否需要被事务代理)它实现解析事务注解判断是否需要进行动态代理,实现事务功能

但是这个问题归根结底还是没有说明,为什么mapper多次数据库修改操作,具备事务。

具备事务的前提是使用同一个连接,这样才能connection.commit提交事务,connnection.rollback,回滚事务,根本原理就在SqlSessionTemplate,对SqlSession的复用中

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

上图展示了mybatis在结合spring后,是如何让自己的sqlsession复用的,存于事务管理器中(基于ThreadLocal,存储事务信息,这也就是为啥多线程情况下事务效的原因)但是还是没有说明为啥复用了同一个connection

接下来我们看下使用SqlSessionFactory(mybatis自动配置类注入的DefaultSqlSessionFactory)开启一个sqlsession的逻辑

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

首先获取TransactionFactory事务工厂,这里使用的是SpringManagedTransactionFactory《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

它返回的事务是SpringManagedTransaction,然后创建一个Executor,然后封装成一个DefaultSqlSession返回,这里我们重点看下SpringManagedTransaction
《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里Connection的获取便是从事务同步管理器的ThreadLocal中获取,如果没有connection一般是第一次数据库操作,那么这里会dataSource.getConnection()开启一个连接,然后交由事务同步管理器处理,后续便会复用此连接。

2.3 spring管理mybatis事务的时候,事务何时提交

上面我们说到数据库操作都交由DefaultSqlSession处理,DefaultSqlSession是一个门面,其提交事务,最终还是调用到了SpringManagedTransactioncommit方法

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里可以看到SpringManagedTransaction#commit只有连接没有交给spring管理,并且连接并非自动提交才会生效,基本上调用这里的提交不会产生任何效果。

上面我们说到SqlSessionTemplate委托动态代理后的SqlSession执行操作的时候,会从事务同步管理器中获取SqlSession,如果没有那么new一个然后注册到事务同步管理器中

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

事务提交的奥秘就在registerSessionHolder

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里的SqlSessionSynchronization是一个TransactionSynchronization对象,TransactionSynchronization接口提供了多个方法在事务不同的时期会在代理对象@Transactional标注的方法中进行回调

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

其中beforeCommit方法会在@Transactional标注的代理对象其业务逻辑执行完成后,如果需要提交事务,会被回调到,这时候就会调用SqlSession的commit方法进行提交事务

提交事务会继续委托给SpringManagedTransaction,可是其commit方法只会在事务不被spring管理的时候进行提交,如果事务被spring管理,@Transactional注解标注的代理对象方法执行后会调用PlatformTransactionManager#commit,这里会调用到DataSourceTransactionManager(如果是分布式事务那么是其他的实现类,基于数据源的事务都是调用此类)它会调用connection.commit 提交事务

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.4不加事务注解的mapper进行数据库操作,事务何时提交

sqlSessionTemplate 中的SqlSessionInterceptor,在创建出SqlSession执行完数据库操作的时候,发现事务没有被Spring管理,此时便会立即提交事务

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

getSqlsession无法从事务同步管理器中复用SqlSession,每次都是new出一个SqlSession 因为当前方法无事务注解,事务同步管理器不会处于活跃状态。提交事务会调用到SpringManagedTransaction其commit方法中判断得到事务没有被spring管理,便会调用connection.commit提交事务

2.5不加事务注解使用mapper执行多条数据库修改操作,会没有事务的原因

public void test(){
   xxxmapper.insert1();
   xxxmapper.insert2();
}

2.4中我们可以看到,insert1insert2的执行,其实每次都会new出一个新的sqlsession,每一个sqlsession对应一个SpringManagedTransaction,每一次执行结束后都会立马提交事务,所有不具备事务。所有那怕test方法最后抛出异常,事务也会提交。

六丶Mybatis操作数据库

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

上面我们研究了mybatis事务和spring事务的结合,并没有关注mybatis是如何进行数据库操作的,下面我们来看下mybatis是怎么拿到xml中的一条sql,把我们入参中的对象映射到sql中的占位符,执行sql然后将结果集解析为mapper方法的出参类型对象的。

前面几节的知识中提到,DefaultSqlSession是mybatis操作数据库的门面,增删改查都是交由它来实现

其中所有的查询操作都是调用select或者selectList方法,所有的新增,更新,删除都是调用update(这些都是数据库变更操作)方法,我们以查询操作为例

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

可以看到查询的操作最终委托给了Executor对象

1.Executor

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

1.1.Executor接口

  • 该接口提供了改和查的基本功能(数据库的删除插入本质也是更新)
  • 提交和回滚
  • 缓存相关方法
  • 批处理刷新
  • 执行器关闭
  • 延迟加载

1.2.BaseExecutor

​ 对Executor中的接口中的大部分方法进行了通用的实现,并且可以通过配置文件,或者手动指定执行器类型来让mybatis使用具体执行器实现(这里说的实现只有BatchExcutor,SimpleExecutor,ReuseExcutor),还提供了三个抽象方法(如下)让子类实现

  • doUpdate
  • doFlushStatements
  • doQuery

1.3.SimpleExecutor

简单执行器,是 MyBatis 中默认使用的执行器,对BaseExecutor中的方法进行了简单的实现,(根据配置获取连接,根据连接获取Statement,执行sql,结果集映射)每执行一次 update 或 select,就开启一个 Statement 对象,用完就直接关闭 Statement 对象

1.4.BatchExecutor

主要应对批量更新,插入,删除,一次向数据库发送多个SQL语句从而减少通信开销,从而提高性能。(对查找不生效)

批量处理允许将相关的SQL语句分组到批处理中,并通过对数据库的一次调用来提交它们,一次执行完成与数据库之间的交互。需要注意的是:JDBC中的批处理只支持 insert、update 、delete 等类型的SQL语句,不支持select类型的SQL语句。

1.5.ReuseExecutor

ReuseExecutor 不同于 SimpleExecutor 的地方在于 ReuseExecutor 维护了 Statement 缓存

ReuseExecutor顾名思义就是重复使用执行,其定义了一个Map<String, Statement>,将执行的sql作为key,将执行的Statement作为value保存,这样执行相同的sql时就可以使用已经存在的Statement,就不需要新创建了,从而避免SimpleExecutor这样多次进行参数拼接生成statement以提高性能

1.6.CachingExecutor

CachingExecutor没有继承BaseExecutor,CachingExecutor 不具备 Executor 执行器功能,CachingExecutor 是一个装饰器, Mybatis 采用装饰者模式对 Executor 执行器提供了功能增强。CachingExecutor装饰器能够使得被装饰的Executor 具备二级缓存功能

下图是Configuration创建Executor的流程,如果全局配置指定了cachEnable,那么会使用CachingExcutor进行装饰,并且mybatis插件可以作用于Excutor,

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

1.7 Executor执行数据库操作流程

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

1.7.1 CachingExcutor装饰器模式实现二级缓存

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

其装饰的作用就是让被装饰的Executor具备二级缓存的能力,在执行查询,更改等操作的时候会维护二级缓存,由于二级缓存并不常用(因为我们基本上都是微服务多实例,一个实例更新了二级缓存,如何同步到其他实例,我们需要自己实现cache,这有带来一致性等问题,一般是不开启二级缓存的)我们不继续深究二级缓存的原理

1.7.2 BaseExcutor模板方法设计模式

BaseExcutor定义了基本的流程,对于子类具备差异的地方,留给子类自己去实现,从而达到高内聚的目的。以查询为例,一级缓存的刷新由BaseExecutor在合适的时机调用,首先从一级缓存中获取,如果缓存中存在,那么不会进行数据库查询操作,反之调用queryFromDatabase查询数据库,queryFromDatabase会调用到doQuery方法,这个方法由子类自己实现

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

至此我们可以解答下 一丶从一个问题开始——读已提交情况下mybatis一级缓存造成的问题,出现的原因便是一级缓存缓存了上一次的查询结果,由于我们执行的是同一个查询,mapperStatement(mapper方法全限定)一致,入参也样一致,也没有内存分页的内容,参数映射等内容也一致,便会命中缓存,所以读已提交的隔离级别,被mybatis 破坏

但是如果我们不加事务直接,便不会如此,因为不加事务直接,每一次查询操作都是new出的sqlsession,都会调用到不同的Executor,一级缓存是和Eexcutor中的一个属性(本质是一个map)这样一级缓存便是不同的对象,便不会命中缓存。

1.7.3 SimpleExecutor 如何查询数据库
这里我们没有研究ReuseExecutor如何复用,其实使用map(key是执行查询的sql,value是statement)达到复用的目的
也没有研究BatchExecutor,本质是执行更改操作的时候调用的是statement#addBatch,批量执行sql语句,
二者使用的都很少,将不做过多赘述了

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

可以看到SimpleExcutor执行查询委托给了StatementHandler,它会用StatmentHandler创建statement,然后执行查询

我们总结下至此的执行流程,如下图

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

接下来我们探究下StatmentHandler是如何进行参数映射,使用Statment执行数据操作,并处理返回结果集的

2.StatmentHandler操作数据库

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.1.StatemenHanlder接口

定义了StatementHandler的基本功能

  • 准备语句 子类可以实现返回不同的Statement子类
  • 参数映射
  • 更新操作
  • 查询操作

2.2BaseStatementHandler

模板方法设计模式,提取公共的操作到父类,子类具备差异的地方使用抽象方法,交由子类实现

2.2RoutingStatementHandler

主要是适配多个StatmentHandler的实现,有点装饰器适配器的意思

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

后续具体方法的实现都是调用delegate对应的方法,相当于RoutingStatementHandler 只是做了一个根据MappedStatement中的StatementType配置创建不同的StatmentHandler

2.3PrepareStatementHandler

预处理Statement的handler,处理带参数允许的SQL, 对应JDBC的PreparedStatement(预编译处理)

2.4 SimpleStatementHandler

最简单的StatementHandler,处理不带参数运行的SQL,对应JDBC的Statement

2.5 CallableStatementHandler

存储过程的Statement的handler,处理存储过程SQL,对应JDBC的CallableStatement(存储过程处理)

下图是mybatis创建一个statementHandler,默认是RoutingStatementHandler,正在操作数据库的一般是PrepareStatmentHandler,并且mybatis插件会发挥作用

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

2.6 PrepareStatementHandler 如何创建一个Statement,并设置参数,执行查询的

2.6.1 prepare

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

最终初始化一个statement是由子类PrepareStatementHandler调用connection.prepareStatement实现

2.6.2 参数映射

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

可以看到参数映射的工作,交给了ParameterHandler(唯一的实现类是DefaultParameterHandler)具体设置参数的流程如下

//设置参数
@Override
public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //1.获取sql语句的参数,ParameterMapping里面包含参数的名称类型等详细信息,还包括类型处理器
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        //2.遍历依次处理
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            //3.OUT类型参数不处理
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;
                //4.获取参数名称
                String propertyName = parameterMapping.getProperty();
                //5.如果propertyName是动态参数,就会从动态参数中取值。(当使用<foreach>的时候,MyBatis会自动生成额外的动态参数)
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    //6.如果参数是null,不管属性名是什么,都会返回null。
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    //7.判断类型处理器是否有参数类型,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
                    value = parameterObject;
                } else {
                    //8.这种情况下是复杂对象或者Map类型,通过反射方便的取值。通过MetaObject操作
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                //9.获取对应的数据库类型
                JdbcType jdbcType = parameterMapping.getJdbcType();
                //空类型
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                //10.对PreparedStatement的占位符设置值(类型处理器可以给PreparedStatement设值)
                try {
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                } catch (SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

可以看到最终使用TypeHandler设置参数,会调用到prepareStatement#setxxx方法设置参数

2.6.3 查询数据库,并转换结果集

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

查询数据库的操作委托给了ResultSetHandler的实现类DefaultResultSetHandler

  • 处理多结果集

存储过程存在多结果集的情况,

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

  • 处理一行结果集

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

不存在嵌套子查询的时候,使用handleRowValuesForSimpleResultMap

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里出现一个类DefaultResultContext,实现了ResultContext,这是结果上下文,主要的职责是控制处理结果行的停止,配合rowBounds实现内存分页,后面的storeObject就是将一行对应的对象存在list(似乎对map这种出惨有特殊处理,对于嵌套子查询也有特殊处理)

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

这里的自动映射应该是处理,没有指定resultMap 凭借对象属性和数据库列名进行映射的情况 ,后面applyPropertyMappings 处理指定resultMap 中column和 property的情况,对于指定了TypeHandler的列,会使用TypeHandler进行设置(调用TypeHandler#getResult),自动映射的类使用MetaObject#setValue处理(反射设置属性)

七丶mybatis插件实现原理

1.拦截器接口

public interface Interceptor {

    //拦截        Invocation :当前被拦截对象 参数 和被拦截方法
  Object intercept(Invocation invocation) throws Throwable;

  //动态代理 
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP  可以在这里给拦截器赋值一些属性 
  }

}

2.Plugin.wrap(target, this)方法如何实现拦截

  • Plugin 实现了InvocationHandler——基于JDK动态代理

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

  • 读取注解信息,获取当前拦截器要拦截什么类的什么方法

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

注意获取方法的方式

Method method = sig.type().getMethod(sig.method(), sig.args());

getMethod方法是没有办法获取到私有方法的,所有无法拦截一个私有方法

  • 获取被代理类的接口

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

  • 动态代理对象生成

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

3.动态代理生成的对象是怎么被使用的

如上我们知道了mybatis 是怎么支持插件的,根据拦截器上的信息生成动态代理对象,动态代理对象在执行方法的时候会进入拦截器的intercept拦截方法,那么动态代理的生成的对象在哪里被使用到昵

  • Configuration 类 也就是mybatis的大管家,在new一些mybatis四大对象的时候会使用到插件

    《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

    《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

    《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

    《SpringBoot源码2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)》

    也就是说mybatis 只支持拦截ParmeterHandler,ResultSetHandler,StatementHandler,Excutor这四种对象
    

    在mybatis执行中的使用的四大对象其实是被动态代理后的对象,从而调用到插件功能

    原文作者:Cuzzz
    原文地址: https://www.cnblogs.com/cuzzz/p/16974644.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞