SpringBoot源码学习2——SpringBoot x Mybatis 原理解析(如何整合,事务如何交由spring管理,mybatis如何进行数据库操作)
阅读本文需要spring源码知识,和springboot相关源码知识
对于springboot 整合mybatis,以及mybatis源码关系不密切的知识,本文将简单带过
- 涉及到spring ioc原理,可移步学习:Spring源码学习笔记12——总结篇IOC,Bean的生命周期,三大扩展点
- 涉及spring aop原理,可参考 Spring 源码学习笔记10——Spring AOP
- 涉及spring 申明式事务,可参考 Spring 源码学习笔记11——Spring事务
- 涉及到springboot自动装配的原理,可参考:SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的
一丶从一个问题开始——读已提交情况下mybatis一级缓存造成的问题
上图中,已知道users1的size为5,那么users2的大小为多少昵?
我们暂且抛弃mybatis框架中的知识,从mysql事务隔离级别进行分析,test方法第一次查询到总数,然后重新开启一个事务插入了一条(require_new的传播级别),后续addOne方法将立即提交,再次查询的时候,test方法应该可以立马查询到已经提交的数据,应该比第一次输出的应该多1,这是事务隔离级别指导我们做出的判断
但是事实上是users1大小和 users2一样大,这是为什么昵?
我们看下控制台
发现mybatis并没有进行第二次数据库的查询,这时候我们应该意识到mybatis
具备缓存,从而导致第二次查询并没有访问数据库
也就是说 读已提交的隔离级别下,mybatis如果不关闭缓存将存在错误
(这里的缓存指的一级缓存,二级缓存普遍是不开的)
具体原理,笔者此文讲到mybatis缓存后将进行解读,下面我们从springboot 和 mybatis整合,到mybatis执行原理展开讲mybatis的原理
二丶mybatis-springboot-starter的自动装配
通常springboot整合mybatis只需要引入如下依赖
简单描述就是SpringBoot启动的时候会读取META-INF/spring.factories中自动配置的类,加入到容器中,后续springboot会将这些类当作配置类进行解析
上图是mybatis-spring-starter的META-INF/spring.factories
,其中关键的是MybatisAutoConfiguration
1.导入SqlSessionTemplate,SqlSessionFactory
这里可以看到当容器中没有SqlSessionFactory
的时候,MybatisAutoConfiguration
会为我们注入一个SqlSessionFactory
,SqlSessionTemplate
同样如此。
这里我们简单提一下SqlSessionFactory
和SqlSessionTemplate
的作用
1.1.mybatis中的SqlSessionFactory
故名思意,它是创建SqlSession
的工厂
这里Spring构建SqlSessionFactory,使用了SqlSessionFactoryBean#getObject
它实现了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
这里可以看到如果没有MapperFactoryBean
和MapperScannerConfigurer
这两个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操作
2.2AutoConfiguredMapperScannerRegistrar
在没有MapperScannerConfigurer
,mybatis自动装配会为我们注入它,它是一个ImportBeanDefinitionRegistrar
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
spring容器在启动的时候,会回调它的postProcessBeanDefinitionRegistry
在这个方法里面会扫描所有的mapper接口,指定其class为MapperFactoryBean
,从而在后续的实例化中,调用MapperFactoryBean#getObject
生成mapper接口的动态代理对象
三丶mybatis扫描mapper接口,注册mapper的Beanfinition
1.MapperScannerConfigurer是如何被注册到spring容器中的
上文中,我们说到,如果我们没有使用@MapperScan
或者@MapperScans
注解标注在配置类上面,那么会默认添加一个MapperScannerConfigurer
,进行mapper接口的扫描注册工作
通常启动类都有这样的@MapperScan
@MapperScan上面存在@Import,会导入一个MapperScannerRegistrar
,这是一个ImportBeanDefinitionRegistrar
会在这里注册MapperScannerConfigurer
的bean定义信息
其实就是把@MapperScan注解上的配置,绑定到MapperScannerConfigurer
的属性上,
@MapperScan注解,可以指定mapper在的包,mapper接口必须标注的注解,Mapper接口动态代理对象生成使用的MapperFactoryBean
等
2.MapperScannerConfigurer 如何进行扫描注册mapper的
其实扫描注册的工作委托给了ClassPathMapperScanner
,调用scan
方法进行扫描注册
它是一个ClassPathBeanDefinitionScanner
的子类,ClassPathBeanDefinitionScanner就是负责包路径扫描,注册BeanDefinition的
这里的扫描调用了ClassPathBeanDefinitionScanner
的doScan方法,这个方法会根据包路径解析成Resouce
对象,然后根据路径下的类包装成BeanDefinition(ScannedGenericBeanDefinition)
重点看下processBeanDefinitions
这里最关键的是definition.setBeanClass(this.mapperFactoryBeanClass)
,即将mapper接口的BeanDefinition类型指定为MapperFactoryBean
,这样在spring后续实例化mapper的时候就调用MapperFactoryBean#getObject
方法进行实例化了
至此我们学习了SpringBoot是如何和mybatis进行结合的,下面总结成一图
四丶Mapper bean的实例化
当我们一个Service需要注入一个mapper的时候,会从Spring容器中找对应的实例,这时候边会涉及到这个mapper的实例化,但是我们mapper明明是一个接口呀,如何实例化昵?
虽然我们mapper是一个接口,但是注入到service属性上的是这个接口的实现类,它是mybatis动态代理后生成的对象。
这个实例化的入口便是AbstractBeanFactory#getBean
方法
1.获取mapper对应的BeanDefinition
这里获取的beanDefinition便是源自ClassPathMapperScanner
注册到容器中的
2.实例化MapperFactoryBean
我们上面说到过,实例化mapper需要调用MapperFactoryBean#getObject
,那么首先需要实例化一个MapperFactoryBean
这里实例化MapperFactoryBean边是使用的createBean
方法,然后Spring会使用反射调用构造方法实例化出MapperFactoryBean(Spring还存在使用CGLIB生成子类然后实例化的方式),其中调用的是
这个构造方法需要一个入参,表示Mapper接口类型,那么这个mapperInterface入参来自那么昵?ClassPathMapperScanner扫描完mapper接口,生成BeanDefinition后,还会在BeanDefinition中记录全限定类型,这个全限定类名将作为MapperFactoryBean的构造器入参
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
其中checkDaoConfig
方法被MapperFactoryBean重写
这里会调用configuration.addMapper
解析xml和mybaits相关的注解,然后进行注册和接口进行绑定,但是这一步解析xml操作通常不会真正进行,因为在创建SqlSessionFactory的时候已经进行了
5.调用MapperFactory#getObject实例化出一个mapper
实例化出一个Mapper接口的动态代理对象,调用的是SqlSessesionTemplate#getMapper
那么到底mapper方法调用的时是如何操作数据库的昵?这一点我们后面继续说
至此我们知道了我们service注入的mapper其实是mybatis使用动态代理生成的对象,表面是一个什么方法实现都没有的接口,其实是动态代理"负重前行",下图展示了一个mapper被创造出来的全流程
五丶Mybatis 和spring事务的结合
上面我们知道了xxMapper其实是一个jdk动态代理生成的对象 ,其InvocationHandler
是MapperProxy
当mapper被调用其接口中声明的方法的时候,会调用到InvocationHandler#invoke
这时候MapperProxy就会大显身手
1.MapperProxy#invoke
MapperProxy内部使用了一个Map缓存方法和对应的执行器(MapperMethodInvoker
),这个map通常来自MapperProxyFactory的ConcurrentHashMap属性。而真正方法的调用又委托给了MapperMethod#execute
,MapperMethod根据方法调用的类型(增删改查)调用MapperProxy中的属性SqlSession(spring环境下的sqlSession实现类是SqlSessionTemplate)`对应的方法
2.SqlSessionTemplate 与mybatis spring事务
SqlSessionTemplate实现了SqlSession
接口,但是真正进行数据库操作的时候,都是委托给属性SqlSessionProxy
,SqlSessionTemplate
存在的意义在于"模板"
——复用SqlSession,那么为什么需要复用,为何要复用?我们接着看下它的构造方法
可以看到,其内部的sqlSessionProxy
是一个动态代理类,我们看下SqlSessionInterceptor
,它是一个InvocationHandler
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的复用中
上图展示了mybatis在结合spring后,是如何让自己的sqlsession复用的,存于事务管理器中(基于ThreadLocal,存储事务信息,这也就是为啥多线程情况下事务效的原因)但是还是没有说明为啥复用了同一个connection
接下来我们看下使用SqlSessionFactory(mybatis自动配置类注入的DefaultSqlSessionFactory)
开启一个sqlsession的逻辑
首先获取TransactionFactory
事务工厂,这里使用的是SpringManagedTransactionFactory
它返回的事务是SpringManagedTransaction
,然后创建一个Executor
,然后封装成一个DefaultSqlSession
返回,这里我们重点看下SpringManagedTransaction
这里Connection
的获取便是从事务同步管理器的ThreadLocal中获取,如果没有connection一般是第一次数据库操作,那么这里会dataSource.getConnection()
开启一个连接,然后交由事务同步管理器处理,后续便会复用此连接。
2.3 spring管理mybatis事务的时候,事务何时提交
上面我们说到数据库操作都交由DefaultSqlSession
处理,DefaultSqlSession
是一个门面,其提交事务,最终还是调用到了SpringManagedTransaction
的commit
方法
这里可以看到SpringManagedTransaction#commit
只有连接没有交给spring管理,并且连接并非自动提交才会生效,基本上调用这里的提交不会产生任何效果。
上面我们说到SqlSessionTemplate委托动态代理后的SqlSession执行操作的时候,会从事务同步管理器中获取SqlSession,如果没有那么new一个然后注册到事务同步管理器中
事务提交的奥秘就在registerSessionHolder
中
这里的SqlSessionSynchronization
是一个TransactionSynchronization
对象,TransactionSynchronization
接口提供了多个方法在事务不同的时期会在代理对象@Transactional标注的方法中进行回调
其中beforeCommit方法会在@Transactional标注的代理对象其业务逻辑执行完成后,如果需要提交事务,会被回调到,这时候就会调用SqlSession的commit方法进行提交事务
提交事务会继续委托给SpringManagedTransaction,可是其commit方法只会在事务不被spring管理的时候进行提交,如果事务被spring管理,@Transactional注解标注的代理对象方法执行后会调用PlatformTransactionManager#commit
,这里会调用到DataSourceTransactionManager
(如果是分布式事务那么是其他的实现类,基于数据源的事务都是调用此类)它会调用connection.commit
提交事务
2.4不加事务注解的mapper进行数据库操作,事务何时提交
sqlSessionTemplate 中的SqlSessionInterceptor,在创建出SqlSession执行完数据库操作的时候,发现事务没有被Spring管理,此时便会立即提交事务
且getSqlsession
无法从事务同步管理器中复用SqlSession,每次都是new出一个SqlSession 因为当前方法无事务注解,事务同步管理器不会处于活跃状态。提交事务会调用到SpringManagedTransaction
其commit方法中判断得到事务没有被spring管理,便会调用connection.commit提交事务
2.5不加事务注解使用mapper执行多条数据库修改操作,会没有事务的原因
public void test(){
xxxmapper.insert1();
xxxmapper.insert2();
}
2.4中我们可以看到,insert1
和insert2
的执行,其实每次都会new出一个新的sqlsession,每一个sqlsession对应一个SpringManagedTransaction
,每一次执行结束后都会立马提交事务,所有不具备事务。所有那怕test
方法最后抛出异常,事务也会提交。
六丶Mybatis操作数据库
上面我们研究了mybatis事务和spring事务的结合,并没有关注mybatis是如何进行数据库操作的,下面我们来看下mybatis是怎么拿到xml中的一条sql,把我们入参中的对象映射到sql中的占位符,执行sql然后将结果集解析为mapper方法的出参类型对象的。
前面几节的知识中提到,DefaultSqlSession是mybatis操作数据库的门面,增删改查都是交由它来实现
其中所有的查询操作都是调用select
或者selectList
方法,所有的新增,更新,删除都是调用update
(这些都是数据库变更操作)方法,我们以查询操作为例
可以看到查询的操作最终委托给了Executor对象
1.Executor
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,
1.7 Executor执行数据库操作流程
1.7.1 CachingExcutor装饰器模式实现二级缓存
其装饰的作用就是让被装饰的Executor具备二级缓存的能力,在执行查询,更改等操作的时候会维护二级缓存,由于二级缓存并不常用(因为我们基本上都是微服务多实例,一个实例更新了二级缓存,如何同步到其他实例,我们需要自己实现cache,这有带来一致性等问题,一般是不开启二级缓存的)我们不继续深究二级缓存的原理
1.7.2 BaseExcutor模板方法设计模式
BaseExcutor定义了基本的流程,对于子类具备差异的地方,留给子类自己去实现,从而达到高内聚的目的。以查询为例,一级缓存的刷新由BaseExecutor在合适的时机调用,首先从一级缓存中获取,如果缓存中存在,那么不会进行数据库查询操作,反之调用queryFromDatabase
查询数据库,queryFromDatabase
会调用到doQuery
方法,这个方法由子类自己实现
至此我们可以解答下 一丶从一个问题开始——读已提交情况下mybatis一级缓存造成的问题,出现的原因便是一级缓存缓存了上一次的查询结果,由于我们执行的是同一个查询,mapperStatement(mapper方法全限定)一致,入参也样一致,也没有内存分页的内容,参数映射等内容也一致,便会命中缓存,所以读已提交的隔离级别,被mybatis 破坏
但是如果我们不加事务直接,便不会如此,因为不加事务直接,每一次查询操作都是new出的sqlsession,都会调用到不同的Executor,一级缓存是和Eexcutor中的一个属性(本质是一个map)这样一级缓存便是不同的对象,便不会命中缓存。
1.7.3 SimpleExecutor 如何查询数据库
这里我们没有研究ReuseExecutor如何复用,其实使用map(key是执行查询的sql,value是statement)达到复用的目的
也没有研究BatchExecutor,本质是执行更改操作的时候调用的是statement#addBatch,批量执行sql语句,
二者使用的都很少,将不做过多赘述了
可以看到SimpleExcutor执行查询委托给了StatementHandler,它会用StatmentHandler创建statement,然后执行查询
我们总结下至此的执行流程,如下图
接下来我们探究下StatmentHandler是如何进行参数映射,使用Statment执行数据操作,并处理返回结果集的
2.StatmentHandler操作数据库
2.1.StatemenHanlder接口
定义了StatementHandler的基本功能
- 准备语句 子类可以实现返回不同的Statement子类
- 参数映射
- 更新操作
- 查询操作
2.2BaseStatementHandler
模板方法设计模式,提取公共的操作到父类,子类具备差异的地方使用抽象方法,交由子类实现
2.2RoutingStatementHandler
主要是适配多个StatmentHandler的实现,有点装饰器适配器的意思
后续具体方法的实现都是调用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插件会发挥作用
2.6 PrepareStatementHandler 如何创建一个Statement,并设置参数,执行查询的
2.6.1 prepare
最终初始化一个statement是由子类PrepareStatementHandler调用connection.prepareStatement
实现
2.6.2 参数映射
可以看到参数映射的工作,交给了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 查询数据库,并转换结果集
查询数据库的操作委托给了ResultSetHandler
的实现类DefaultResultSetHandler
- 处理多结果集
存储过程存在多结果集的情况,
- 处理一行结果集
不存在嵌套子查询的时候,使用handleRowValuesForSimpleResultMap
这里出现一个类DefaultResultContext,实现了ResultContext,这是结果上下文,主要的职责是控制处理结果行的停止,配合rowBounds实现内存分页,后面的storeObject就是将一行对应的对象存在list(似乎对map这种出惨有特殊处理,对于嵌套子查询也有特殊处理)
这里的自动映射应该是处理,没有指定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动态代理
- 读取注解信息,获取当前拦截器要拦截什么类的什么方法
注意获取方法的方式
Method method = sig.type().getMethod(sig.method(), sig.args());
getMethod方法是没有办法获取到私有方法的,所有无法拦截一个私有方法
- 获取被代理类的接口
- 动态代理对象生成
3.动态代理生成的对象是怎么被使用的
如上我们知道了mybatis 是怎么支持插件的,根据拦截器上的信息生成动态代理对象,动态代理对象在执行方法的时候会进入拦截器的intercept拦截方法,那么动态代理的生成的对象在哪里被使用到昵
-
Configuration 类 也就是mybatis的大管家,在new一些mybatis四大对象的时候会使用到插件
也就是说mybatis 只支持拦截ParmeterHandler,ResultSetHandler,StatementHandler,Excutor这四种对象
在mybatis执行中的使用的四大对象其实是被动态代理后的对象,从而调用到插件功能