启动MyBatis时要解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了我们怎么控制MyBatis的行为,和我们要对数据库下达的指令,也就是我们的SQL信息。我们会把它们解析成一Configuration对象。
接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数
据库之间的一次连接:这个就是SqISession对象。
我 们 要 获 得 一 个 会 话 ,必 须 有 一 个 会 话 工 厂 SqISessionFactory。 SqISessionFactory里面又必须包含我们的所有的配置信息,所以我们会通过一个
Builder来创建工厂类。
MyBatis是对JDBC的封装,也就是意味着底层一定会出现JDBC的一些核心对象,
比如执行SQL的 Statement,结果集ResultSet。在 Mybatis里面,SqISession只是提
供给应用的一个接口,还不是SQL的真正的执行对象。
SqISession持有了一个Executor对象,用来封装对数据库的操作。
在执行器Executor执行query或者update操作的时候我们创建一系列的对象, 来处理参数、执行SQL、处理结果集,这里我们把它简化成— 对象:StatementHandler, 可以把它理解为对Statement的封装,在阅读源码的时候我们再去了解还有什么其他的
对象。
按照功能职责的不同,所有的package可以分成不同的工作层次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBjzFGEr-1589355509875)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510153506611.png)]
首先接口层是我们打交道最多的。核心对象是SqISession,它是上层应用和MyBatis 打交道的桥梁,SqlSession±定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。
核心处理层主要做了这几件事:
基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml解析、反射、IO、 事务等等这些功能。
cache缓存
cache缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate一样,MyBatis也有一级缓存和二级缓存,并且预留了集成第三方
缓存的接口。
缓存体系结构
MyBatis跟缓存相关的类都在cache包里面,其中有一个Cache接口,只有一个默
认的实现类PerpetualCache,它是用HashMap实现的。
PerpetualCache这个对象一定会创建,所以这个叫做基础缓存。但是缓存又可以有 很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给
基础缓存加上这些功能,如果不需要,就不加。
除了基础缓存之外,MyBatis也定义了很多的装饰器,同样实现了 Cache接口,通 过这些装饰器可以额外实现很多的功能。
// 煎饼加鸡蛋加香肠
以
“装饰者模式(Decorator Pattern)是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹 性的替代方案(扩展原有对象的功能)。”
debug源码的时候,有可能会看到基础缓存被装饰四五层,当然不管怎么装饰,经 过多少层装饰’最后使用的还是基本的实现类(默认PerpetualCache)。
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。
—级缓存也叫本地缓存(Local Cache), MyBatis的一级缓存是在会话(SqISession)
层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何的配置
(localCacheScope 设置为 STATEMENT 关闭一级缓存)。
首先我们必须去弄清楚一个问题,在 MyBatis执行的流程里面,涉及到这么多的对 象’那么缓存PerpetualCache应该放在哪个对象里面去维护?
如果要在同一个会话里面共享一级缓存,最好的办法是在SqISession里面创建的,
作为SqISession的一个属性,跟 SqISession共存亡,这样就不需要为SqISession编III 号、
再根据SqISession的编号去查找对应的缓存了。
DefaultSqISession 里面只有两个对象属性:Configuration 和 Executoro
Configuration是全局的,不属于SqISession,所以缓存只可能放在Executor里面 维护----实际上它是在基本执行器 SimpleExecutor/ReuseExecutor/BatchExecutor 的 父类BaseExecutor的构造函数中持有了 PerpetualCache
在同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不
会再发送SQL到数据库。但是不同的会话里面,即使执行的SQL一模一样(通过一个
Mapper的同一个方法的相同参数调用),也不能使用到一级缓存。
接下来我们来验证一下,MyBatis的一级缓存到底是不是只能在一个会话里面共享, 以及跨会话(不同session)操作相同的数据会产生什么问题。
注意演示一级缓存需要先关闭二级缓存,localCacheScope设置为SESSION。
怎么判断是否命中缓存?
如果再次发送SQL到数据库执行(控制台打印了 SQL语
句 ),说明没有命中缓存;如果直接打印对象,说明是从内存缓存中取到了结果。
—级缓存在什么时候put,什么时候get,什么时候clear?
—级缓存在 BaseExecutor 的 query。——queryFromDatabase()中存入。在 queryFromDatabase 之前会 get()
—级缓存怎么命中? CacheKey怎么构成?
BaseExecutor 的 queryFromDatabase()
一级缓存什么时候会被清空呢?
同一个会话中,update (包括delete)会导致一级缓存被清空
只有更新会清空缓存吗?查询会清空缓存吗?如果要清空呢?
—级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的 (无条件),如果是query会判断(只有select标签的HushCache=true才清空)。
一级缓存的工作范围是一个会话。如果跨会话,会出现什么问题?
其他会话更新了数据,导致读取到过时的数据(一级缓存不能跨会话共享)
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别 的,可以被多个SqISession共 享 (只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。
思考一个问题:如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是
在一级缓存之后呢?二级缓存是在哪里维护的呢?
作为一个作用范围更广的缓存,它肯定是在SqISession的夕卜层,否则不可能被多个
SqISession 共享。
而一级缓存是在SqISession内部的,所以第一个问题,肯定是工作在一级缓存之前, 也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqISession本 身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创
建一个对象。
但是,二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象,
如果没有,就不用这个对象,我们应该怎么做呢?就好像你的煎饼果子要加鸡蛋就加鸡
重,要加火腿就加火腿(又来了)……
实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutoro 如果启用了二级缓存,MyBatis在 创 建 Executor对象的时候会对Executor进行装饰。
CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接 返回,如果没有委派交给真正的查询器Executor实现类,比 如 SimpleExecutor来执行 查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
我们知道,一级缓存是默认开启的,那二级缓存怎么开启呢?我们来看下二级缓存
的开启方式。
第一步:在 mybatis-config.xml中 配 置 了 (可以不配9 置,默认是true):
<setting name="cacheEnabled" value="true"/>
只要没有显式地设置cacheEnabled=false,都 会 用 CachingExecutor装饰基本的执 行 器 (SIMPLE. REUSE、BATCH)。
二级缓存的总开关是默认开启的。但是每个Mapper的二级缓存开关是默认关闭的。 一 个 Mapper要用到二级缓存,还要单独打开它自己的开关。
第二步:在 Mapper.xml中配置
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" >
eviction="LRU" <!—回收策略-->
flushInterval="120000" <!— 自动刷新时间ms ,未配置时只有调用时刷新——>
readOnly="false"/> <!—默认是false (安全),改为true可读写时,对象必须支持序列化-->
cache属性详解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uA8RFwdQ-1589355509878)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510184419636.png)]
Mapper.xml 配 置了
会刷新缓存。
如果二级缓存拿到结果了,就直接返回(最外层的判断),否则再到一级缓存,最
后到数据库。
如 果 cacheEnabled=true, Mapper.xml没有配置
只要cacheEnabled=true基本执行器就会被装饰。有没有配置
query方法里面的判断:
if (cache != null) {
也就是说,此时会装饰,但是没有cache对象,依然不会走二级缓存流程。
如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时 性要求很高,不需要二级缓存,怎么办?
我们可以在单个Statement ID上显式关闭二级缓存(默认是true):
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
CachingExecutor query方法有对这个属性的判断:
(验证二级缓存需要先开启二级缓存)
1、 事务不提交,二级缓存不存在
思考:为什么事务不提交,二级缓存不生效?
因为二级缓存使用TransactionalCacheManager (TCM )来管理,最后又调用了 TransactionalCache 的 getObject()、 putObject 和 commit方法,TransactionalCache
里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache
在 putObject的时候,只是添加到了 entriesToAddOnCommit里面,只有它的 commit方法被调用的时候才会调用flushPendingEntries()真正写入缓存。它就是在 DefaultSqISession调用commit()的时候被调用的。
2、使用不同的session和 mapper,并且提交事务,验证二级缓存可以跨session
存在
3、在其他的session中执行增删改操作,验证缓存会被刷新
为什么增删改操作会清空缓存?
在CachingExecutor的update方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面渠道的flushCache的值。而增删改操作的 flushCache属性默认为true。
也就是说,如果不需要清空二级缓存,可以把flushCache属性修改为false (这样 会造成过时数据的问题)。
什么时候开启二级缓存?
一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问
题,在什么情况下才有必要去开启二级缓存?
1、 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主
的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
2、 如果多个namespace中有针对于同一个表的操作,比如Blog表,如果在一个 namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情
况。
所以,推荐在一个Mapper里面只操作单表的情况使用。
如果要让多个namespace共享一个二级缓存,应该怎么做?
跨 namespace的缓存共享的问题,可以使用
<cache-ref namespace="com.gupaoedu.crud.dao.DepartmentMapper" />
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。
注意:在这种情况下,多个Mapper的操作都会弓I起缓存刷新,缓存的意义已经不
大了。
除了 MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级
缓存。
MyBatis官方提供了一些第三方缓存集成方式,比如ehcache和 redis: https://github.com/mybatis/reclis-cache
pom文件引入依赖:
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-redisartifactId>
<version>1.0.0-beta2version>
dependency>
Mapper.xml 配置,type 使用 RedisCache:
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
redis.properties 酉己置:
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
当然’在分布式环境中,我们也可以使用独立的缓存服务 不使用MyBatis自带的
二级缓存。
分析源码,我们还是从编程式的demo入手。
@Before
public void prepare() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
/**
* 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
* @throws IOException
*/
@Test
public void testSelect() throws IOException {
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
把文件读取成流的这一步我们就省略了。所以下面我们分成五步来分析。
第一步,我们创建一个工厂类,配置文件的解析就是在这一步完成的,包括
mvbatis-config.xml 和 Mapper 映射器文件。
这一步我们关心的内容:解析的时候做了什么,产生了什么对象,解析的结果存放
到了哪里。解析的结果决定着我们后面有什么对象可以使用,和到哪里去取。
第二步,通过 SqISessionFactory 创建一个 SqlSession
问题:SqISession上面定义了各种增删改查的API, 是给客户端调用的。返回了什么实现类?除了 SqISession,还创建了什么对象,创建了什么环境?
第三步,获得一个Mapper对象。
问题:Mapper是一个接口,没有实现类,是不能被实例化的,那获取到的这个 Mapper对象是什么对象?为什么要从SqISession里面去获取?为什么传进去一个接口,然后还要用接口类型来接收?
第四步,调用接口方法。
问题:我们的接口没有创建实现类,为什么可以调用它的方法?那它调用的是什么
方法?
这一步实际做的事情是执行SQ L,那它又是根据什么找到XML映射器里面的SQL
的? 此外,我们的方法参数(对象或者Map)是怎么转换成SQL参数的?获取到的结 果集是怎么转换成对象的?
最后一步,关闭session,这一步是必须要做的。
F面我们会按照这五个步骤,去理解MyBatis的运行原理,这里面会涉及到很多核心的对象和关键的方法。
1、 一定要带着问题去看,猜想验证。
2、 不要只记忆流程,学编程风格,设计思想(他的代码为什么这么写?如果不这么写
呢?包括接口的定义,类的职责,涉及模式的应用,高级语法等等)。
3、 先抓重点,就像开车熟路,哪个地方限速,哪个地方变道,要走很多次才会熟练。
先走主干道,再去覆盖分支小路。
4、 记录核心流程和对象,总结层次、结构、关系,输 出 (图片或者待注释的源码)。
5、 培养看源码的信心和感觉,从带着看到自己去看,看更多的源码。
6、 debug还是直接Ctrl+Alt+B跟方法?
debug可以看到实际的值,比如到底是哪个实现类,value到底是什么。
首先我们要清楚的是配您 置解析的过程全部只解析了两种文件。一个是
mybatis-config.xml全局配置文件。另外就是所有的Mapper.xml文件,也包括在
Mapper接口类上面定义的注解。
我们从mybatis-config.xml开始。在第一节课的时候我们已经分析了核心配置了, 大概明白了 MyBatis有哪些配置项,和这些配9 置项的大致含义。这里我们再具体看一下 这里面的标签都是怎么解析的,解析的时候做了什么。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先我们new 了一个SqISessionFactoryBuilder,这是建造者模式的运用(建造者模式用来创建复杂对象,而不需要关注内部细节,是一种封装的体现)。MyBatis中很 多地方用到了建造者模式(名字以Builder结尾的类还有9个)。
SqISessionFactoryBuilder 中用来创建 SqISessionFactory 对象的方法是 build(),build方 法 有 9 个 重 载 ,可以用不同的方式来创建sqISessionFactory对象。
SqISessionFactory对象默认是单例的。
这 里 面 创 建 了 一 个 XMLConfigBuilder对 象 (用来存放所有配置信息的
Configuration对象也是这个时候创建的)。
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
XMLConfigBuilder是抽象类BaseBuilder的一个子类,专门用来解析全局配置文
件,针对不同的构建目标还有其他的一些子类(关联到源码路径),比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增删改查标签
XMLScriptBuilder:解析动态 SQL
根据我们解析的文件流,这里后面两个参数都是空的,创建了一个parser。
return build(parser.parse());
这里有两步,第一步是调用parser的 parse。方法’它会返回一个Configuration
类。
之前我们说过,也就是配置文件里面所有的信息都会放在Configuration里面。
我们先看一下parse方法:
首先会检查全局配置文件是不是已经解析过,也就是说在应用的生命周期里面,
config配置文件只需要解析一次,生成的Configuration对象也会存在应用的整个生命
周期中。
接下来就是 parseConfiguration 方法:
parseConfiguration(parser.evalNode("/configuiation"));
解析XML有很多方法,MyBatis对 dom和 SAX做了封装,方便使用。
这下面有十几个方法,对应着config文件里面的所有一级标签。
问题:MyBatis全局配置文件中标签的顺序可以颠倒吗?比如把settings放在 p山gin之后?会报错。所以顺序必须严格一致。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8w9yH3ds-1589355509887)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510192403100.png)]
第一个是解析<properties>标 签 ,读取我们引入的外部配置文件,例如
db.properties
这里面又有两种类型,一种是放在resource目录下的,是相对路径,一种是写的绝对路径的(url)。
解析的最终结果就是我们会把所有的配置信息放到名为defaults的 Properties对象
里面(Hashtable 对象,KV 存储),最后把 XPathParser 和 Configuration 的 Properties
属性都设置成我们填充后的Properties对象。
parser.setVariables(defaults);
configuration.setVariables(defaults);
第二个,我们把〈settings〉标签也解析成了一个Properties对象,对于〈settings〉
标签的子标签的处理在后面(先解析,后设置)。
Properties settings = settingsAsProperties(root.evalNode("settings"));
在早期的版本里面解析和设置都是在后面一起的,这里先解析成Properties对象是 因为下面的两个方法要用到。
loadCustomVfs是获取Vitual File System 的自定义实现类,比如要读取本地文件,
或者FTP远程文件的时候,就可以用到自定义的VFS类。
根据〈settings〉标签里面的
MyBatis中有JBoss6VFS和 DefaultVFS两个实现,在 io 包中。
Class<? extends VFS> vfslmpl = (Class<? extends VFS>)Resources.classForName(clazz);
configuration.setVfslmpl(vfslmpl);
最后赋值到Configuration中。
loadCustomLoglmpI是根据
Class<? extends Log> loglmpl = resolveClassCprops.getProperty("loglmpl"));
configuration. setLoglmpl(loglmpl);
这里生成了一个Log接口的实现类,并且赋值到Configuration中。
这一步解析的是类型别名。
我们在讲配置的时候也讲过,它有两种定义方式,一种是直接定义一个类的别名(例
如 com.domain.Blog定义成b lo g ), —种就是指定一个package ,那么这个包下面所
有的类的名字就会成为这个类全路径的别名。
类的别名和类的关系,我们放在一个TypeAliasRegistry对象里面。
tpeAliasRegistry.registerAlias(alias, clazz);
大家也可以推测一下,如果要保存这种类名(String)和 类 (Class)的对应关系,
TypeAliasRegistry应该是一个什么样的数据结构。
这一步是解析<environments>标签
我们前面讲过,一个environment就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>创建一个事务工厂,根据 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRfTmoqR-1589355509889)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510193754091.png)] 解析databaseldProvider标签,生成DatabaseldProvider对 象 (用来支持不同厂 跟 TypeAlias—样,TypeHandler有两种配置方式,一种是单独配置一个类,一种 问题:这种三个对象(Java类型,JDBC类型,Handler)的关系怎么映射? (Map http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers 最后就是<mappers>标签的解析。 根据全局配置文件中不同的注册方式,用不同的方式扫描,但最终都是做了两件事 先从解析 Mapper.xml 的 mapperParser.parse()方法入手。 configurationElement---- 解析所有的子标签,最终获得MappedStatement对 bindMapperForNamespace()---- 把 namespace (接口 类 型 ) 和 工 厂 类 MapperProxyFactory 绑定起来。 1) configurationElement() configuration Element是 对 Mapper.xml中 所 有 具 体 标 签 的 解 析 ,包括 namespace, cache、parameterMap、resultMap、sql 和 select|insert|update|delete 在 buildStatementFromContext。方法中,创建了用来解析增删改查标签的 XMLStatementBuilder,并且把创建的 MappedStatement 添加至ij mappedStatements 中。 2) bindMapperForNamespace() 主要是是调用了 addMapper() addMapper方法中,把接口类型注册到MapperRegistry中:实际上是为接口创 建 一 个 对 应 的 MapperProxyFactory (用 于 为 这 个 type提 供工厂类 创建 MapperProxy)。 注 册 了 接 口 之 后 ,开 始 解 析 接 口 类 和 所 有 方 法 上 的 注 解 ,例如 此处创建了一个MapperAnnotationBuilder专门用来解析注解。 parse方 法 中 的 parseCache()和 parseCacheRef()方 法 其 实 是 对 @CacheNamespace 和 @CacheNamespaceRef 这两个注解的处理。 parseStatement()方法里面的各种getAnnotation,都是对相应的注解的解析, 比如@Options, @SelectKey,@ResultMap 等等。 最后同样会创建MappedStatement对象,添加到MapperRegistry中。也就是说在XML中配置,和使用注解配置,最后起到一样的效果。 4) build Mapper.xml解析完之后,调用另一个build()方法,返 回 SqISessionFactory的默 认实现类 DefaultSqlSessionFactory 总结 在这一步,我们主要完成了 config配置文件、Mapper文件、Mapper接口中注解 我们得到了一个最重要的对象Configuration,这里面存放了全部的配置信息,它在 属性里面还有各种各样的容器。 最后,返回了一个 DefaultSqISessionFactory,里面持有了 Configuration 的实例。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xd2sd6LH-1589355509890)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510201250500.png)] 程序每一次操作数据库,都需要创建一个会话,我们用openSession()方法来创建。 里 用 到 了 上 一 步 创 建 的 DefaultSqISessionFactory , 在 openSessionFromDataSource()方法中创建。 这个会话里面,需要包含一个Executor用来执行SQL。Executor又要指定事务类型和执行器的类型。 所以我们会先从Configuration里面拿到Enviroment, Enviroment里面就有事务 这里会从Environment对象中取出一个TransactionFactory,它是解析 事务工厂类型可以配置成JDBC或者MANAGED 如果配置的是 JDBC,则会使用 Connection 对象的 commit、rollback、close() 如果9配置成MANAGED,会把事务交给容器来管理,比如JBOSS, Weblogic。因 如 果 是 Spring + MyBatis , 则 没 有 必 要 配 置 , 因 为 我 们 会 直 接 在 使用newExecutor方法创建: 可以细分成三步。 1 )创建执行器 Executor 的 基 本 类 型 有 三 种 :SIMPLE、BATCH、R EU SE,默认是 SIMPLE (settingsElement()读取默认值)。 他们都继承了抽象类BaseExecutor。 抽象类实现了 Executor接口。 为什么要让抽象类BaseExecutor实现Executor接口,然后让具体实现类继承抽象 这是模板方法的体现。 抽象方法是在子类中实现的,BaseExecutor最终会调用到具体的子类。 2)缓存装饰 如果cacheEnabled=true ,会用装饰器模式对executor进行装饰。 3)插件代理 装饰完毕后,会执行: 此处会对executor植入插件逻辑。 4)返回SqISession实现类 最终返回 DefaultSqISession,它的属性包括 Configuration. Executor 对象。 创建会话的过程,我们获得了一个DefaultSqISession,里面包含了一个Executor, Executor是 SQL的实际执行对象。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOtANbGP-1589355509892)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510203132329.png)] 在 旧 版 的 MyBatis中 ,DefaultSqISession的 selectOne()方法可以直接根据 Mappenxml中的StatementID ,找到SQL执行。但是这种方式属于硬编码,我们没办 另一个问题是如果参数传入错误,在编译阶段也是不会报错的,不利于预先发现问 在 MyBatis后期的版本提供了第二种调用方式,就是定义一个接口,然后再调用 由于我们的接口名称跟Mapper.xml的 namespace是对应的,接口的方法跟 statement ID也都是对应的,所以根据方法就能找到对应的要执行的SQL。 这里有两个问题需要解决: 1、getMapper获得的是一个什么对象?为什么可以执行它的方法? 2、到底是怎么根据Mapper找到XML中的SQL执行的? DefaultSqISession 的 getMapper()方法,调用了 Configuration 的 getMapper Configuration 的 getMapper()方法,又调用了 MapperRegistry 的 getMapper方法。 我们知道,在解析mapper标签和Mapper.xml的时候已经把接口类型和类型对应 在 newlnstance()方法中,先创建 MapperProxy MapperProxy 实现了 InvocationHandler 接口,主要属性有三个:sqISession、 mapperinterface、methodCache。 最终通过JDK动态代理模式创建、返回代理对象: 也就是说,getMapperQ返回的是一个JDK动 态 代 理 对 象 ( 类 型 数 字 ) 。 这个代理对象会继承Proxy类,实现被代理的接口,里面持有了一个MapperProxy类 回答了前面的问题:为什么要在MapperRegistry中保存一个工厂类/原来它是用 这里是代理模式的一个非常经典的应用 但是为什么要直接代理一个接口呢? 我们知道,JDK的动态代理,有三个核心角色:被代理类(实现类)、接口、实现 被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6gMNO49-1589355509895)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510204834382.png)] 而 MyBatis里面的Mapper没有实现类,怎么被代理呢?它忽略了实现类,直接对 MyBatis的动态代理: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uF1xLXpu-1589355509896)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510204916531.png)] 在 MyBatis里面,动态代理为什么不需要实现类呢? 这里我们要想想我们的目的。我们的目的是根据一个可以执行的方法,直接找到 如果根据接口类型+ 方法的名称找到Statement ID 这个逻辑在Handler类 (MapperProxy)中就可以完成,其实也就没有实现类的什么事了。 获得Mapper对象的过程,实质上是获取了 一个JDK动态代理对象(类型是$ Proxy 数字)。这个代理类会继承Proxy类,实现被代理的接口,里面持有了一个MapperProxy 类型的触发管理类。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yFeWOIVm-1589355509897)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510205019344.png)] 由于所有的Mapper都是JDK动态代理对象,所以任意的方法都是执行触发管理类 问题1:我们引入MapperProxy为了解决什么问题?硬编码和编译时检查问题。它 需要做的事情是:根据方法查找Statement ID的问题。 问题2 :进入到invoke方法的时候做了什么事情?它是怎么找到我们要执行的SQL 我们看一下invoke方法: 1 )首先判断是否需要去执行SQ L,还是直接执行方法。Object本身的方法不需要 2 )获取缓存 Map的 computelfAbsent()方法:根 据 key获取值,如果值是n u ll,则把后面 Object的值赋给key。 Java8和 Java9 中的接口默认方法有特殊处理,返回DefaultMethodlnvoker。 普通的方法返回的是PlainMethodlnvoker,返回MapperMethod。 MapperMethod中有两个主要的属性: 一个 是 SqlCommand , 封 装 了 statement id ( 例 如 : com.gupaoedu.mapper.BlogMapper.selectBlogByld)和 SQL 类型。 —个是Methodsignature,主要是主要封装是返回值的类型。 这两个属性都是MapperMethod的内部类。 另外MapperMethod种定义了多种executeQ方法。 接下来又调用了 mapperMethod的 execute方法: 在这一步,根据不同的type (INSERT. UPDATE. DELETE. SELECT)和返回类型: 调用convertArgsToSqlCommandParam。将方法参数转换为SQL的参数。 调用 sqISession 的 insert、update、delete、selectOne ()方法。我们以 这里来到了对外的接口的默认实现类DefaultSqlSession。 selectOne()最终也是调用了 selectList() 在 SelectList中,我们先根据 command name (Statement ID)从 Configuration 中拿到MappedStatemento ms里面有xml中增删改查标签配置的所有属性,包括id、 statementType、sqISource、useCache.入参、出参等等。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o45uDS8S-1589355509899)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510205703683.png)] 然后执行了 Executor的 query方法。 Executor是第二步openSession的时候创建的,创建了执行器基本类型之后,依次执行了二级缓存装饰,和插件拦截。 所以,如果有被插件拦截,这里会先走到插件的逻辑。如果没有显式地在settings 1 ) 创建 CacheKey 二级缓存的CacheKey是怎么构成的呢?或者说,什么样的查询才能确定是同一个 在 BaseExecutor的 createCacheKey方法中,用到了六个要素: 也就是说,方法相同、翻页偏移相同、SQL相同、参数值相同、数据源环境相同, CacheKey的实际值举例(toString生成的),debug可以看到: 注意看一下CacheKey的属性,里面有一个List按顺序存放了这些要素。 怎么比较两个CacheKey是否相等呢?如果一上来就是依次比较六个要素是否相等, 要比较6次,这样效率不高。有没有更高效的方法呢?继承Object的每个类,都有一个 hashCode ()方法,用来生成哈希码。它是用来在集合中快速判重的。 在生成CacheKey的时候(update方 法 ),也更新了 CacheKey的 hashCode,它 是用乘法哈希生成的(基数baseHashCode=17 ,乘法因子multiplier=37 )。 Object中的hashCode。是一个本地方法,通过随机数算法生成(OpenJDK8 , 默 认,可以通过-XX:hashCode修 改 )。CacheKey中的hashCodeQ方法进行了重写,返 回自己生成的hashCode。 为什么要用37作为乘法因子呢?跟 String中的31类似。 CacheKey中的equals也进行了重写,比较CacheKey是否相等。 如果哈希值(乘法哈希)、校验值(加法哈希)、要素个数任何一个不相等,都不 CacheKey生成之后,调用另一个query方法。 2)处理二级缓存 首先从ms中取出cache对象,判断cache对象是否为空,如果为空,则没有查询 二级缓存、写入二级缓存的流程。 Cache对象是什么时候创建的呢? 用来解析 Mapper.xml 的 XMLMapperBuilder 类,cacheElement方法: 只有Mapper.xml中的 此处创建了一个Cache对象。 大家可以自行验证一下,注释 二级缓存为什么要用TCM来管理? 我们来思考一个问题,在一个事务中: 1、 首先插入一条数据(没有提交),此时二级缓存会被清空。 2、 在这个事务中查询数据,写入二级缓存。 3、 提交事务,出现异常,数据回滚。 此时出现了数据库没有这条数据,但是二级缓存有这条数据的情况。所以MyBatis 的二级缓存需要跟事务关联起来。 疑问:为什么一级缓存不这么做? 因为一个session就是一个事务,事务回滚,会话就结束了,缓存也清空了,不存 在读到一级缓存中脏数据的情况。二级缓存是跨session的,也就是跨事务的,才有可 能出现对同一个方法的不同事务访问。 1 )写入二级缓存 从 map中拿出TransactionalCache对象,把 value添加到待提交的Map。 此时缓 只有事务提交的时候缓存才真正写入(close或者commit最后分析)。 2 ) 获取二级缓存 从 map中拿出TransactionalCache对象,这个对象也是对PerpetualCache经过 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0vamVCH-1589355509900)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510212633616.png)] 得到再getObject(), 这个是一个会递归调用的方法,直到到达PerpetualCache, 1)清空本地缓存 querystack用于记录查询栈,防止递归查询重复处理缓存。 flushCache=true的时候,会先清理本地缓存(—级缓存): 如果没有缓存,会从数据库查询:queryFromDatabase 如果 LocalCacheScope == STATEMENT,会清理本地缓存。 2)从数据库查询 a)缓存 先在缓存用占位符占位。执行查询后,移除占位符,放入数据。 b)查询 执行 Executor 的 doQuery; 默认是 SimpleExecutor 1)创建 StatementHandler 在 configuration.newStatementHandler(), new—个 StatementHandler, 先 得到 RoutingStatementHandler RoutingStatementHandler里 面 没 有 任 何 的 实 现 ,是 用 来 创 建 基 本 的 StatementHandler 的。这里会根据 MappedStatement 里面的 statementType 决定 StatementHandler 的 类 型 。 默认是 PREPARED ( STATEMENT. PREPARED、 StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的 这两个对象都是在上面new的时候创建的。 这三个对象都是可以被插件拦截的四大对象之一,所以在创建之后都要用拦截器进 至此,四大对象的包装已经全部完成。 P S :四大对象还有一个是谁?在什么时候创建的? (Executor) 2 ) 创建 Statement 用 new 出来的 StatementHandler 创建 Statement 对象。 如果有插件包装,会先走到被拦截的业务逻辑。 prepareStatement()方法对语句进行预编译,处理参数: 这里面会调用parameterHandler设置参数,如果有插件包装,会先走到被拦截的 执行的 StatementHandler 的 query方法 RoutingStatementHandler 白勺 query方法 。 delegate 委派,最终执行 PreparedStatementHandler 的 query()方法。 4 ) 执行 PreparedStatement 的 execute方法 后面就是JDBC包中的PreparedStatement的执行了。 如果有插件包装,会先走到被拦截的业务逻辑。 问题:怎么把ResultSet转换成List ResultSetHandler 只有一个实现类:DefaultResultSetHandler。也就是执行 DefaultResultSetHandler 的 handleResultSets ()方法。 首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况, 然后会调用handleResultSet方法。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXDz17MO-1589355509903)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510214703265.png)] 参考资料: 1.咕泡学院·MyBatis体系结构与工作原理·青山5.8.databaseldProviderElement()
商的数据库)。5.9.typeHandlerElement()
是指定一个package。最后我们得到的是JavaType和 JdbcType,以及用来做相互映射 的 TypeHandler之间的映射关系,存放在TypeHandlerRegistry对象里面。typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
里面再放一个Map)5.10.mapperElement()
情,对于语句的注册和接口的注册。 private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 不同的定义方式的扫描,最终都是调用 addMapper()方法(添加到 MapperRegistry)。这个方法和 getMapper() 对应
// package 包
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// resource 相对路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析 Mapper.xml,总体上做了两件事情 >>
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// url 绝对路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// class 单个接口
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
public void parse() {
// 总体上做了两件事情,对于语句的注册和接口的注册
if (!configuration.isResourceLoaded(resource)) {
// 1、具体增删改查标签的解析。
// 一个标签一个MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(接口类型)和工厂类绑定起来,放到一个map。
// 一个namespace 一个 MapperProxyFactory >>
bindMapperForNamespace();
}
}
象。 private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 添加缓存对象
cacheRefElement(context.evalNode("cache-ref"));
// 解析 cache 属性,添加缓存对象
cacheElement(context.evalNode("cache"));
// 创建 ParameterMapping 对象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 创建 List
MappedStatement statement = statementBuilder.build();
// 最关键的一步,在 Configuration 添加了 MappedStatement >>
configuration.addMappedStatement(statement);
return statement;
configuration.addMapper(boundType);
knownMappers.put(type, new MapperProxyFactoiy<>(type));
@CacheNamespace、 @SelectMapperAimotationBuilder parser = new MappeiAiinotationBuilder(config, type);
parser.parse();
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 先判断 Mapper.xml 有没有解析,没有的话先解析 Mapper.xml(例如定义 package 方式)
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 处理 @CacheNamespace
parseCache();
// 处理 @CacheNamespaceRef
parseCacheRef();
// 获取所有方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 解析方法上的注解,添加到 MappedStatement 集合中 >>
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
// 最后 增删改查标签 也要添加到 MappedStatement 集合中
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
public SqlSessionFactoiy build(Configuration config) {
return new DefaultSqlSessionFactoiy(config);
}
的解析。6.会话创建过程
SqlSession session = sqlSessionFactory.openSession();
工厂。6.1.创建 Transaction
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
管理事务。
为我们跑的是本地程序,如果配置成MANAGE不会有任何事务。
您
applicationContext.xml里面配置数据源和事务管理器,覆盖MyBatis的配您 置。6.2.创建 Executor
// 根据事务工厂和默认的执行器类型,创建执行器 >>
final Executor executor = configuration.newExecutor(tx, execType);
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默认 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
}
类?模板方法定义一个算法的骨架,并允许子类为一个或者多个步骤提供实现。模板方法使得子类可以在不
改变算法结构的情况下,重新定义算法的某些步骤。
@Override
protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
throw new UnsupportedOperationException("Not supported.");
}
@Override
protected List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
throw new UnsupportedOperationException("Not supported.");
}
@Override
protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
throw new UnsupportedOperationException("Not supported.");
}
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
return new DefaultSqlSession(configuration, executor, autoCommit);
6.3.总结
7.获得Mapper对象
法知道有多少处调用,修改起来也很麻烦。
题。Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlogById", 1);
Mapper接口的方法。BlogMapper mapper = session.getMapper(BlogMapper.class);
7.1.getMapper()
方法。configuration.<T>getMapper()
return mapperRegistry.getMapper(type, sqlSession);
的 MapperProxyFactory放到了一个Map中。获取Mapper代理对象 实际上是从
Map中获取对应的工厂类后,调用以下方法创建对象: @SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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);
}
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
型的触发管理类。
来创建返回代理类的。7.2.MapperProxy如何实现对接口的代理
了 InvocationHandler的触发管理类,用来生成代理对象。
接口进行代理。
Mapper.xml 中的 Statement ID ,方便调用。7.3.总结
8.执行SQL
Blog blog = mapper.selectBlog(1);
MapperProxy 的 invoke()方法。
的?8.1.MapperProxy. invoke()
去执行 S Q L,比如 toString、hashCode、equals、getClass()
这里加入缓存是为了提升MapperMethod的获取速度。很巧妙的设计。缓存的使 用在MyBatis中随处可见。// 获取缓存,保存了方法签名和接口方法的关系
final MapperMethod mapperMethod = cachedMapperMethod(method);
private final SqlCommand command;
private final MethodSignature method;
8.2.MapperMethod. execute()
mapperMethod.execute(sqlSession, args);
查询为例,会走到selectOne。方法。Object param = method. convertArgsToSqlCommandParam( args);
result = sqISession.selectOne(command.getName(), param);
8.3.DefaultSqlSession. selectOne()
@Override
public <T> T selectOne(String statement, Object parameter) {
// 来到了 DefaultSqlSession
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
中配置cacheEnabled=false,再走到CachingExecutor的逻辑,然后会走到BaseExecutor 的 query方法。8.4.CachingExecutor. query()
查询呢? cacheKey.update(ms.getId()); // com.gupaoedu.mapper.BlogMapper.selectBlogById
cacheKey.update(rowBounds.getOffset()); // 0
cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
cacheKey.update(boundSql.getSql());
才会被认为是同一个查询。// -1381545870:4796102018:com.gupaoedu.mapper.BlogMapper.selectBlogById:0:2147483647:select * from blog where bid = ?:1:development
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;
hashcode = multiplier * hashcode + baseHashCode;
@Override
public boolean equals(Object object) {
// 同一个对象
if (this == object) {
return true;
}
// 被比较的对象不是 CacheKey
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
// hashcode 不相等
if (hashcode != cacheKey.hashcode) {
return false;
}
// checksum 不相等
if (checksum != cacheKey.checksum) {
return false;
}
// count 不相等
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
是同一个查询,最后才循环比较要素,防止哈希碰撞。 Cache cache = ms.getCache();
// cache 对象是在哪里创建的? XMLMapperBuilder类 xmlconfigurationElement()
// 由
cacheElement(context.evalNode("cache"));
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
tcm.putObject(cache, key, list);
存还没有真正地写入。public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
List<E> list = (List<E>) tcm.getObject(cache, key);
层层装饰的缓存对象:
拿到value。public Obj ect getObject(Object key) {
return cache.get(key);
}
8.5.BaseExecutor.query ()
if (queiyStack == 0 && msJsFlushCacheReqiiiredO) { clearLocalCache();
}
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
localCache.putObject(key, EXECUTION_PLACEHOLDER);
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
8.6.SimpleExecutor. doQuery
CALLABLE) public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// StatementType 是怎么来的? 增删改查标签中的 statementType="PREPARED",默认值 PREPARED
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 创建 StatementHandler 的时候做了什么? >>
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
ResultSetHandler // 创建了四大对象的其它两大对象 >>
// 创建这两大对象的时候分别做了什么?
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
行包装的方法。 // 植入插件逻辑(返回代理对象)
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
// 植入插件逻辑(返回代理对象)
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
// 植入插件逻辑(返回代理对象)
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
// 用Connection创建一个Statement
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
业务逻辑。 @Override
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}
return resultSetHandler.handleResultSets(ps);
—般只有一个结果集。如果下面的这个while循环我们也不用,就是执行一次。9.MyBatis核心对象