前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >29.MyBatis体系结构与工作原理

29.MyBatis体系结构与工作原理

作者头像
编程之心
发布2020-08-12 16:06:31
8810
发布2020-08-12 16:06:31
举报
文章被收录于专栏:编程之禅编程之禅

1.MyBatis的工作流程分析

一、解析配置文件

启动MyBatis时要解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了我们怎么控制MyBatis的行为,和我们要对数据库下达的指令,也就是我们的SQL信息。我们会把它们解析成一Configuration对象。

二、提供操作接口

接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数 据库之间的一次连接:这个就是SqISession对象。

我 们 要 获 得 一 个 会 话 ,必 须 有 一 个 会 话 工 厂 SqISessionFactory。 SqISessionFactory里面又必须包含我们的所有的配置信息,所以我们会通过一个 Builder来创建工厂类。

MyBatis是对JDBC的封装,也就是意味着底层一定会出现JDBC的一些核心对象, 比如执行SQL的 Statement,结果集ResultSet。在 Mybatis里面,SqISession只是提 供给应用的一个接口,还不是SQL的真正的执行对象。

三、执行SQL操作

SqISession持有了一个Executor对象,用来封装对数据库的操作。

在执行器Executor执行query或者update操作的时候我们创建一系列的对象, 来处理参数、执行SQL、处理结果集,这里我们把它简化成— 对象:StatementHandler, 可以把它理解为对Statement的封装,在阅读源码的时候我们再去了解还有什么其他的 对象。

四、MyBatis主要的工作流程

image-20200510153614345
image-20200510153614345

2.MyBatis架构分层与模块划分(总)

image-20200510153539904
image-20200510153539904

按照功能职责的不同,所有的package可以分成不同的工作层次。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBjzFGEr-1589355509875)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510153506611.png)]

2.1.接口层

首先接口层是我们打交道最多的。核心对象是SqISession,它是上层应用和MyBatis 打交道的桥梁,SqlSession±定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

2.2.核心处理层

核心处理层主要做了这几件事:

  1. 把接口中传入的参数解析并且映射成JDBC类型;
  2. 解析xml文件中的SQL语句,包括插入参数,和动态SQL的生成;
  3. 执 行 SQL语句;
  4. 处理结果集,并映射成Java对象。

2.3.基础支持层

基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml解析、反射、IO、 事务等等这些功能。

3.MyBatis缓存详解

cache缓存

cache缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate一样,MyBatis也有一级缓存和二级缓存,并且预留了集成第三方 缓存的接口。

缓存体系结构

MyBatis跟缓存相关的类都在cache包里面,其中有一个Cache接口,只有一个默 认的实现类PerpetualCache,它是用HashMap实现的。

PerpetualCache这个对象一定会创建,所以这个叫做基础缓存。但是缓存又可以有 很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给 基础缓存加上这些功能,如果不需要,就不加。

除了基础缓存之外,MyBatis也定义了很多的装饰器,同样实现了 Cache接口,通 过这些装饰器可以额外实现很多的功能。

// 煎饼加鸡蛋加香肠 以 “装饰者模式(Decorator Pattern)是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹 性的替代方案(扩展原有对象的功能)。”

image-20200510153931440
image-20200510153931440

debug源码的时候,有可能会看到基础缓存被装饰四五层,当然不管怎么装饰,经 过多少层装饰’最后使用的还是基本的实现类(默认PerpetualCache)。

image-20200510154036531
image-20200510154036531

所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

image-20200510154345315
image-20200510154345315

3.1.一级缓存

—级缓存也叫本地缓存(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的同一个方法的相同参数调用),也不能使用到一级缓存。

image-20200510165308334
image-20200510165308334

接下来我们来验证一下,MyBatis的一级缓存到底是不是只能在一个会话里面共享, 以及跨会话(不同session)操作相同的数据会产生什么问题。

3.2.一级缓存验证

注意演示一级缓存需要先关闭二级缓存,localCacheScope设置为SESSION。

怎么判断是否命中缓存?

如果再次发送SQL到数据库执行(控制台打印了 SQL语 句 ),说明没有命中缓存;如果直接打印对象,说明是从内存缓存中取到了结果。

—级缓存在什么时候put,什么时候get,什么时候clear?

—级缓存在 BaseExecutor 的 query。——queryFromDatabase()中存入。在 queryFromDatabase 之前会 get()

—级缓存怎么命中? CacheKey怎么构成?

BaseExecutor 的 queryFromDatabase()

一级缓存什么时候会被清空呢?

同一个会话中,update (包括delete)会导致一级缓存被清空

只有更新会清空缓存吗?查询会清空缓存吗?如果要清空呢?

—级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的 (无条件),如果是query会判断(只有select标签的HushCache=true才清空)。

一级缓存的工作范围是一个会话。如果跨会话,会出现什么问题?

其他会话更新了数据,导致读取到过时的数据(一级缓存不能跨会话共享)

3.3.二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别 的,可以被多个SqISession共 享 (只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。

思考一个问题:如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是 在一级缓存之后呢?二级缓存是在哪里维护的呢?

作为一个作用范围更广的缓存,它肯定是在SqISession的夕卜层,否则不可能被多个 SqISession 共享。

而一级缓存是在SqISession内部的,所以第一个问题,肯定是工作在一级缓存之前, 也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqISession本 身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创 建一个对象。

但是,二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象, 如果没有,就不用这个对象,我们应该怎么做呢?就好像你的煎饼果子要加鸡蛋就加鸡 重,要加火腿就加火腿(又来了)……

实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutoro 如果启用了二级缓存,MyBatis在 创 建 Executor对象的时候会对Executor进行装饰。

CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接 返回,如果没有委派交给真正的查询器Executor实现类,比 如 SimpleExecutor来执行 查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

image-20200510183951715
image-20200510183951715

我们知道,一级缓存是默认开启的,那二级缓存怎么开启呢?我们来看下二级缓存 的开启方式。

3.4.开启二级缓存的方法

第一步:在 mybatis-config.xml中 配 置 了 (可以不配9 置,默认是true):

代码语言:javascript
复制
<setting name="cacheEnabled" value="true"/>

只要没有显式地设置cacheEnabled=false,都 会 用 CachingExecutor装饰基本的执 行 器 (SIMPLE. REUSE、BATCH)。

二级缓存的总开关是默认开启的。但是每个Mapper的二级缓存开关是默认关闭的。 一 个 Mapper要用到二级缓存,还要单独打开它自己的开关。

第二步:在 Mapper.xml中配置<cache/>标签:

代码语言:javascript
复制
<!-- 声明这个namespace使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
       size="1024" <!—最多缓存对象个数,默认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 配 置了<cache>之 后 ,select会被缓存。update、delete、insert 会刷新缓存。

如果二级缓存拿到结果了,就直接返回(最外层的判断),否则再到一级缓存,最 后到数据库。

如 果 cacheEnabled=true, Mapper.xml没有配置<cache>标签,还有二级缓存吗?还会出现CachingExecutor包装对象吗?

只要cacheEnabled=true基本执行器就会被装饰。有没有配置<cache>,决定了在 启动的时候会不会创建这个mapper的 Cache对象,最终会影响到CachingExecutor query方法里面的判断:

代码语言:javascript
复制
if (cache != null) {

也就是说,此时会装饰,但是没有cache对象,依然不会走二级缓存流程。

如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时 性要求很高,不需要二级缓存,怎么办?

我们可以在单个Statement ID上显式关闭二级缓存(默认是true):

代码语言:javascript
复制
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">

CachingExecutor query方法有对这个属性的判断:

3.5.二级缓存验证

(验证二级缓存需要先开启二级缓存)

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>来解决:

代码语言:javascript
复制
<cache-ref namespace="com.gupaoedu.crud.dao.DepartmentMapper" />

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:在这种情况下,多个Mapper的操作都会弓I起缓存刷新,缓存的意义已经不 大了。

3.6.第三方缓存做二级缓存

除了 MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级 缓存。

MyBatis官方提供了一些第三方缓存集成方式,比如ehcache和 redis: https://github.com/mybatis/reclis-cache

pom文件引入依赖:

代码语言:javascript
复制
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

Mapper.xml 配置,type 使用 RedisCache:

代码语言:javascript
复制
<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

redis.properties 酉己置:

代码语言:javascript
复制
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

当然’在分布式环境中,我们也可以使用独立的缓存服务 不使用MyBatis自带的 二级缓存。

4.MyBatis源码解读

4.1.带着问题去看源码

分析源码,我们还是从编程式的demo入手。

代码语言:javascript
复制
@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的运行原理,这里面会涉及到很多核心的对象和关键的方法。

4.2.看源码的注意事项

1、 一定要带着问题去看,猜想验证。

2、 不要只记忆流程,学编程风格,设计思想(他的代码为什么这么写?如果不这么写 呢?包括接口的定义,类的职责,涉及模式的应用,高级语法等等)。

3、 先抓重点,就像开车熟路,哪个地方限速,哪个地方变道,要走很多次才会熟练。 先走主干道,再去覆盖分支小路。

4、 记录核心流程和对象,总结层次、结构、关系,输 出 (图片或者待注释的源码)。

5、 培养看源码的信心和感觉,从带着看到自己去看,看更多的源码。

6、 debug还是直接Ctrl+Alt+B跟方法? debug可以看到实际的值,比如到底是哪个实现类,value到底是什么。

5.配置解析过程

首先我们要清楚的是配您 置解析的过程全部只解析了两种文件。一个是 mybatis-config.xml全局配置文件。另外就是所有的Mapper.xml文件,也包括在 Mapper接口类上面定义的注解。

我们从mybatis-config.xml开始。在第一节课的时候我们已经分析了核心配置了, 大概明白了 MyBatis有哪些配置项,和这些配9 置项的大致含义。这里我们再具体看一下 这里面的标签都是怎么解析的,解析的时候做了什么。

代码语言:javascript
复制
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

首先我们new 了一个SqISessionFactoryBuilder,这是建造者模式的运用(建造者模式用来创建复杂对象,而不需要关注内部细节,是一种封装的体现)。MyBatis中很 多地方用到了建造者模式(名字以Builder结尾的类还有9个)。

SqISessionFactoryBuilder 中用来创建 SqISessionFactory 对象的方法是 build(),build方 法 有 9 个 重 载 ,可以用不同的方式来创建sqISessionFactory对象。 SqISessionFactory对象默认是单例的。

5.1.XMLConfigBuilder

这 里 面 创 建 了 一 个 XMLConfigBuilder对 象 (用来存放所有配置信息的 Configuration对象也是这个时候创建的)。

代码语言:javascript
复制
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

XMLConfigBuilder是抽象类BaseBuilder的一个子类,专门用来解析全局配置文 件,针对不同的构建目标还有其他的一些子类(关联到源码路径),比如:

XMLMapperBuilder:解析 Mapper 映射器

XMLStatementBuilder:解析增删改查标签

XMLScriptBuilder:解析动态 SQL

image-20200510192150025
image-20200510192150025

根据我们解析的文件流,这里后面两个参数都是空的,创建了一个parser。

代码语言:javascript
复制
return build(parser.parse());

这里有两步,第一步是调用parser的 parse。方法’它会返回一个Configuration 类。

之前我们说过,也就是配置文件里面所有的信息都会放在Configuration里面。<configuration>的子标签跟Configuration类的属性是直接对应的。

我们先看一下parse方法:

首先会检查全局配置文件是不是已经解析过,也就是说在应用的生命周期里面, config配置文件只需要解析一次,生成的Configuration对象也会存在应用的整个生命 周期中。

接下来就是 parseConfiguration 方法:

代码语言:javascript
复制
parseConfiguration(parser.evalNode("/configuiation"));

解析XML有很多方法,MyBatis对 dom和 SAX做了封装,方便使用。

这下面有十几个方法,对应着config文件里面的所有一级标签。

image-20200510192338791
image-20200510192338791

问题:MyBatis全局配置文件中标签的顺序可以颠倒吗?比如把settings放在 p山gin之后?会报错。所以顺序必须严格一致。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8w9yH3ds-1589355509887)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510192403100.png)]

5.2.propertiesElement()

第一个是解析<properties>标 签 ,读取我们引入的外部配置文件,例如 db.properties

这里面又有两种类型,一种是放在resource目录下的,是相对路径,一种是写的绝对路径的(url)。

解析的最终结果就是我们会把所有的配置信息放到名为defaults的 Properties对象 里面(Hashtable 对象,KV 存储),最后把 XPathParser 和 Configuration 的 Properties 属性都设置成我们填充后的Properties对象。

代码语言:javascript
复制
parser.setVariables(defaults); 
configuration.setVariables(defaults);

5.3.settingsAsProperties()

第二个,我们把〈settings〉标签也解析成了一个Properties对象,对于〈settings〉 标签的子标签的处理在后面(先解析,后设置)。

代码语言:javascript
复制
Properties settings = settingsAsProperties(root.evalNode("settings"));

在早期的版本里面解析和设置都是在后面一起的,这里先解析成Properties对象是 因为下面的两个方法要用到。

5.4.loadCustomVfs(settings)

loadCustomVfs是获取Vitual File System 的自定义实现类,比如要读取本地文件, 或者FTP远程文件的时候,就可以用到自定义的VFS类。

根据〈settings〉标签里面的<vfslmpl>标签,生成了一个抽象类V FS 的子类,在 MyBatis中有JBoss6VFS和 DefaultVFS两个实现,在 io 包中。

代码语言:javascript
复制
Class<? extends VFS> vfslmpl = (Class<? extends VFS>)Resources.classForName(clazz); 
configuration.setVfslmpl(vfslmpl);

最后赋值到Configuration中。

5.5.loadCustomLoglmpl(settings)

loadCustomLoglmpI是根据<loglmpl>标签获取日志的实现类,我们可以用到很 多的日志的方案,包括LOG4Jz LOG4J2, SLF4J等等,在 logging包中。

代码语言:javascript
复制
Class<? extends Log> loglmpl = resolveClassCprops.getProperty("loglmpl"));
configuration. setLoglmpl(loglmpl);

这里生成了一个Log接口的实现类,并且赋值到Configuration中。

5.6.typeAliasesElement

这一步解析的是类型别名。

我们在讲配置的时候也讲过,它有两种定义方式,一种是直接定义一个类的别名(例 如 com.domain.Blog定义成b lo g ), —种就是指定一个package ,那么这个包下面所 有的类的名字就会成为这个类全路径的别名。

类的别名和类的关系,我们放在一个TypeAliasRegistry对象里面。

代码语言:javascript
复制
tpeAliasRegistry.registerAlias(alias, clazz);

大家也可以推测一下,如果要保存这种类名(String)和 类 (Class)的对应关系, TypeAliasRegistry应该是一个什么样的数据结构。

5.7.pluginElement

这一步是解析<environments>标签

我们前面讲过,一个environment就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据 源,最后把这两个对象设置成Environment对象的属性,放到Configuration里面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRfTmoqR-1589355509889)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510193754091.png)]

5.8.databaseldProviderElement()

解析databaseldProvider标签,生成DatabaseldProvider对 象 (用来支持不同厂 商的数据库)。

5.9.typeHandlerElement()

跟 TypeAlias—样,TypeHandler有两种配置方式,一种是单独配置一个类,一种 是指定一个package。最后我们得到的是JavaType和 JdbcType,以及用来做相互映射 的 TypeHandler之间的映射关系,存放在TypeHandlerRegistry对象里面。

代码语言:javascript
复制
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);

问题:这种三个对象(Java类型,JDBC类型,Handler)的关系怎么映射? (Map 里面再放一个Map)

5.10.mapperElement()

http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers

最后就是<mappers>标签的解析。

根据全局配置文件中不同的注册方式,用不同的方式扫描,但最终都是做了两件事 情,对于语句的注册和接口的注册。

image-20200510193951441
image-20200510193951441
代码语言:javascript
复制
  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.");
          }
        }
      }
    }
  }

先从解析 Mapper.xml 的 mapperParser.parse()方法入手。

代码语言:javascript
复制
  public void parse() {
    // 总体上做了两件事情,对于语句的注册和接口的注册
    if (!configuration.isResourceLoaded(resource)) {
      // 1、具体增删改查标签的解析。
      // 一个标签一个MappedStatement。 >>
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 2、把namespace(接口类型)和工厂类绑定起来,放到一个map。
      // 一个namespace 一个 MapperProxyFactory >>
      bindMapperForNamespace();
    }
  }

configurationElement---- 解析所有的子标签,最终获得MappedStatement对 象。

bindMapperForNamespace()---- 把 namespace (接口 类 型 ) 和 工 厂 类 MapperProxyFactory 绑定起来。

1) configurationElement()

configuration Element是 对 Mapper.xml中 所 有 具 体 标 签 的 解 析 ,包括 namespace, cache、parameterMap、resultMap、sql 和 select|insert|update|delete

代码语言:javascript
复制
  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<ResultMapping>
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析可以复用的SQL
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析增删改查标签,得到 MappedStatement >>
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

在 buildStatementFromContext。方法中,创建了用来解析增删改查标签的 XMLStatementBuilder,并且把创建的 MappedStatement 添加至ij mappedStatements 中。

代码语言:javascript
复制
    MappedStatement statement = statementBuilder.build();
    // 最关键的一步,在 Configuration 添加了 MappedStatement >>
    configuration.addMappedStatement(statement);
    return statement;

2) bindMapperForNamespace()

主要是是调用了 addMapper()

代码语言:javascript
复制
configuration.addMapper(boundType);

addMapper方法中,把接口类型注册到MapperRegistry中:实际上是为接口创 建 一 个 对 应 的 MapperProxyFactory (用 于 为 这 个 type提 供工厂类 创建 MapperProxy)。

代码语言:javascript
复制
knownMappers.put(type, new MapperProxyFactoiy<>(type));

注 册 了 接 口 之 后 ,开 始 解 析 接 口 类 和 所 有 方 法 上 的 注 解 ,例如 @CacheNamespace、 @Select

此处创建了一个MapperAnnotationBuilder专门用来解析注解。

代码语言:javascript
复制
MapperAimotationBuilder parser = new MappeiAiinotationBuilder(config, type); 
parser.parse();

parse方 法 中 的 parseCache()和 parseCacheRef()方 法 其 实 是 对 @CacheNamespace 和 @CacheNamespaceRef 这两个注解的处理。

代码语言:javascript
复制
  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();
  }

parseStatement()方法里面的各种getAnnotation,都是对相应的注解的解析, 比如@Options, @SelectKey,@ResultMap 等等。

最后同样会创建MappedStatement对象,添加到MapperRegistry中。也就是说在XML中配置,和使用注解配置,最后起到一样的效果。

代码语言:javascript
复制
      // 最后 增删改查标签 也要添加到 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);

4) build

Mapper.xml解析完之后,调用另一个build()方法,返 回 SqISessionFactory的默 认实现类 DefaultSqlSessionFactory

代码语言:javascript
复制
public SqlSessionFactoiy build(Configuration config) { 
    return new DefaultSqlSessionFactoiy(config);
}

总结

在这一步,我们主要完成了 config配置文件、Mapper文件、Mapper接口中注解 的解析。

我们得到了一个最重要的对象Configuration,这里面存放了全部的配置信息,它在 属性里面还有各种各样的容器。

最后,返回了一个 DefaultSqISessionFactory,里面持有了 Configuration 的实例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xd2sd6LH-1589355509890)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510201250500.png)]

6.会话创建过程

程序每一次操作数据库,都需要创建一个会话,我们用openSession()方法来创建。

代码语言:javascript
复制
SqlSession session = sqlSessionFactory.openSession();

里 用 到 了 上 一 步 创 建 的 DefaultSqISessionFactory , 在 openSessionFromDataSource()方法中创建。

这个会话里面,需要包含一个Executor用来执行SQL。Executor又要指定事务类型和执行器的类型。

所以我们会先从Configuration里面拿到Enviroment, Enviroment里面就有事务 工厂。

6.1.创建 Transaction

这里会从Environment对象中取出一个TransactionFactory,它是解析 <environments>标签的时候创建的。

代码语言:javascript
复制
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);

事务工厂类型可以配置成JDBC或者MANAGED

image-20200510202122122
image-20200510202122122

如果配置的是 JDBC,则会使用 Connection 对象的 commit、rollback、close() 管理事务。

如果9配置成MANAGED,会把事务交给容器来管理,比如JBOSS, Weblogic。因 为我们跑的是本地程序,如果配置成MANAGE不会有任何事务。

如 果 是 Spring + MyBatis , 则 没 有 必 要 配 置 , 因 为 我 们 会 直 接 在 您 applicationContext.xml里面配置数据源和事务管理器,覆盖MyBatis的配您 置。

6.2.创建 Executor

使用newExecutor方法创建:

代码语言:javascript
复制
// 根据事务工厂和默认的执行器类型,创建执行器 >>
final Executor executor = configuration.newExecutor(tx, execType);

可以细分成三步。

1 )创建执行器

Executor 的 基 本 类 型 有 三 种 :SIMPLE、BATCH、R EU SE,默认是 SIMPLE (settingsElement()读取默认值)。

代码语言:javascript
复制
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);
    }
}

他们都继承了抽象类BaseExecutor。 抽象类实现了 Executor接口。

image-20200510202632919
image-20200510202632919

为什么要让抽象类BaseExecutor实现Executor接口,然后让具体实现类继承抽象 类?

这是模板方法的体现。

代码语言:javascript
复制
模板方法定义一个算法的骨架,并允许子类为一个或者多个步骤提供实现。模板方法使得子类可以在不
改变算法结构的情况下,重新定义算法的某些步骤。

抽象方法是在子类中实现的,BaseExecutor最终会调用到具体的子类。

代码语言:javascript
复制
    @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.");
    }

2)缓存装饰

如果cacheEnabled=true ,会用装饰器模式对executor进行装饰。

代码语言:javascript
复制
    // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }

3)插件代理

装饰完毕后,会执行:

代码语言:javascript
复制
    // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);

此处会对executor植入插件逻辑。

4)返回SqISession实现类

最终返回 DefaultSqISession,它的属性包括 Configuration. Executor 对象。

代码语言:javascript
复制
return new DefaultSqlSession(configuration, executor, autoCommit);

6.3.总结

创建会话的过程,我们获得了一个DefaultSqISession,里面包含了一个Executor, Executor是 SQL的实际执行对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOtANbGP-1589355509892)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510203132329.png)]

7.获得Mapper对象

在 旧 版 的 MyBatis中 ,DefaultSqISession的 selectOne()方法可以直接根据 Mappenxml中的StatementID ,找到SQL执行。但是这种方式属于硬编码,我们没办 法知道有多少处调用,修改起来也很麻烦。

另一个问题是如果参数传入错误,在编译阶段也是不会报错的,不利于预先发现问 题。

代码语言:javascript
复制
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlogById", 1);

在 MyBatis后期的版本提供了第二种调用方式,就是定义一个接口,然后再调用 Mapper接口的方法。

由于我们的接口名称跟Mapper.xml的 namespace是对应的,接口的方法跟 statement ID也都是对应的,所以根据方法就能找到对应的要执行的SQL。

代码语言:javascript
复制
BlogMapper mapper = session.getMapper(BlogMapper.class);

这里有两个问题需要解决:

1、getMapper获得的是一个什么对象?为什么可以执行它的方法?

2、到底是怎么根据Mapper找到XML中的SQL执行的?

7.1.getMapper()

DefaultSqISession 的 getMapper()方法,调用了 Configuration 的 getMapper 方法。

代码语言:javascript
复制
configuration.<T>getMapper()

Configuration 的 getMapper()方法,又调用了 MapperRegistry 的 getMapper方法。

代码语言:javascript
复制
return mapperRegistry.getMapper(type, sqlSession);

我们知道,在解析mapper标签和Mapper.xml的时候已经把接口类型和类型对应 的 MapperProxyFactory放到了一个Map中。获取Mapper代理对象 实际上是从 Map中获取对应的工厂类后,调用以下方法创建对象:

代码语言:javascript
复制
  @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);
    }
  }

在 newlnstance()方法中,先创建 MapperProxy

MapperProxy 实现了 InvocationHandler 接口,主要属性有三个:sqISession、 mapperinterface、methodCache。

代码语言:javascript
复制
public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
image-20200510204545042
image-20200510204545042

最终通过JDK动态代理模式创建、返回代理对象:

也就是说,getMapperQ返回的是一个JDK动 态 代 理 对 象 ( 类 型 数 字 ) 。 这个代理对象会继承Proxy类,实现被代理的接口,里面持有了一个MapperProxy类 型的触发管理类。

回答了前面的问题:为什么要在MapperRegistry中保存一个工厂类/原来它是用 来创建返回代理类的。

这里是代理模式的一个非常经典的应用

但是为什么要直接代理一个接口呢?

7.2.MapperProxy如何实现对接口的代理

我们知道,JDK的动态代理,有三个核心角色:被代理类(实现类)、接口、实现 了 InvocationHandler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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里面,动态代理为什么不需要实现类呢?

这里我们要想想我们的目的。我们的目的是根据一个可以执行的方法,直接找到 Mapper.xml 中的 Statement ID ,方便调用。

如果根据接口类型+ 方法的名称找到Statement ID 这个逻辑在Handler类 (MapperProxy)中就可以完成,其实也就没有实现类的什么事了。

7.3.总结

获得Mapper对象的过程,实质上是获取了 一个JDK动态代理对象(类型是$ Proxy 数字)。这个代理类会继承Proxy类,实现被代理的接口,里面持有了一个MapperProxy 类型的触发管理类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yFeWOIVm-1589355509897)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510205019344.png)]

8.执行SQL

代码语言:javascript
复制
Blog blog = mapper.selectBlog(1);

由于所有的Mapper都是JDK动态代理对象,所以任意的方法都是执行触发管理类 MapperProxy 的 invoke()方法。

问题1:我们引入MapperProxy为了解决什么问题?硬编码和编译时检查问题。它 需要做的事情是:根据方法查找Statement ID的问题。

问题2 :进入到invoke方法的时候做了什么事情?它是怎么找到我们要执行的SQL 的?

我们看一下invoke方法:

8.1.MapperProxy. invoke()

1 )首先判断是否需要去执行SQ L,还是直接执行方法。Object本身的方法不需要 去执行 S Q L,比如 toString、hashCode、equals、getClass()

2 )获取缓存 这里加入缓存是为了提升MapperMethod的获取速度。很巧妙的设计。缓存的使 用在MyBatis中随处可见。

代码语言:javascript
复制
// 获取缓存,保存了方法签名和接口方法的关系
final MapperMethod mapperMethod = cachedMapperMethod(method);

Map的 computelfAbsent()方法:根 据 key获取值,如果值是n u ll,则把后面 Object的值赋给key。

Java8和 Java9 中的接口默认方法有特殊处理,返回DefaultMethodlnvoker。

普通的方法返回的是PlainMethodlnvoker,返回MapperMethod。

MapperMethod中有两个主要的属性:

代码语言:javascript
复制
private final SqlCommand command; 
private final MethodSignature method;

一个 是 SqlCommand , 封 装 了 statement id ( 例 如 : com.gupaoedu.mapper.BlogMapper.selectBlogByld)和 SQL 类型。

—个是Methodsignature,主要是主要封装是返回值的类型。

这两个属性都是MapperMethod的内部类。

另外MapperMethod种定义了多种executeQ方法。

8.2.MapperMethod. execute()

接下来又调用了 mapperMethod的 execute方法:

代码语言:javascript
复制
mapperMethod.execute(sqlSession, args);
image-20200510205458405
image-20200510205458405

在这一步,根据不同的type (INSERT. UPDATE. DELETE. SELECT)和返回类型:

  1. 调用convertArgsToSqlCommandParam。将方法参数转换为SQL的参数。
  2. 调用 sqISession 的 insert、update、delete、selectOne ()方法。我们以 查询为例,会走到selectOne。方法。
代码语言:javascript
复制
Object param = method. convertArgsToSqlCommandParam( args); 
result = sqISession.selectOne(command.getName(), param);

8.3.DefaultSqlSession. selectOne()

这里来到了对外的接口的默认实现类DefaultSqlSession。

selectOne()最终也是调用了 selectList()

代码语言:javascript
复制
  @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;
    }
  }

在 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 中配置cacheEnabled=false,再走到CachingExecutor的逻辑,然后会走到BaseExecutor 的 query方法。

8.4.CachingExecutor. query()

1 ) 创建 CacheKey

二级缓存的CacheKey是怎么构成的呢?或者说,什么样的查询才能确定是同一个 查询呢?

在 BaseExecutor的 createCacheKey方法中,用到了六个要素:

代码语言:javascript
复制
    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());

也就是说,方法相同、翻页偏移相同、SQL相同、参数值相同、数据源环境相同, 才会被认为是同一个查询。

CacheKey的实际值举例(toString生成的),debug可以看到:

代码语言:javascript
复制
// -1381545870:4796102018:com.gupaoedu.mapper.BlogMapper.selectBlogById:0:2147483647:select * from blog where bid = ?:1:development

注意看一下CacheKey的属性,里面有一个List按顺序存放了这些要素。

代码语言:javascript
复制
  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;

怎么比较两个CacheKey是否相等呢?如果一上来就是依次比较六个要素是否相等, 要比较6次,这样效率不高。有没有更高效的方法呢?继承Object的每个类,都有一个 hashCode ()方法,用来生成哈希码。它是用来在集合中快速判重的。

在生成CacheKey的时候(update方 法 ),也更新了 CacheKey的 hashCode,它 是用乘法哈希生成的(基数baseHashCode=17 ,乘法因子multiplier=37 )。

代码语言:javascript
复制
hashcode = multiplier * hashcode + baseHashCode;

Object中的hashCode。是一个本地方法,通过随机数算法生成(OpenJDK8 , 默 认,可以通过-XX:hashCode修 改 )。CacheKey中的hashCodeQ方法进行了重写,返 回自己生成的hashCode。

为什么要用37作为乘法因子呢?跟 String中的31类似。

CacheKey中的equals也进行了重写,比较CacheKey是否相等。

代码语言:javascript
复制
  @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;
  }

如果哈希值(乘法哈希)、校验值(加法哈希)、要素个数任何一个不相等,都不 是同一个查询,最后才循环比较要素,防止哈希碰撞。

CacheKey生成之后,调用另一个query方法。

2)处理二级缓存

首先从ms中取出cache对象,判断cache对象是否为空,如果为空,则没有查询 二级缓存、写入二级缓存的流程。

代码语言:javascript
复制
    Cache cache = ms.getCache();
    // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
    // 由 <cache> 标签决定
    if (cache != null) {
        
    }

Cache对象是什么时候创建的呢?

用来解析 Mapper.xml 的 XMLMapperBuilder 类,cacheElement方法:

代码语言:javascript
复制
cacheElement(context.evalNode("cache"));

只有Mapper.xml中的<cache>标签不为空才解析。

代码语言:javascript
复制
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);

此处创建了一个Cache对象。

代码语言:javascript
复制
    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();

大家可以自行验证一下,注释<cache>,是否不解析,cache是否为null。开启 <cache>,是否解析,cache是否不为null。

二级缓存为什么要用TCM来管理?

我们来思考一个问题,在一个事务中:

1、 首先插入一条数据(没有提交),此时二级缓存会被清空。

2、 在这个事务中查询数据,写入二级缓存。

3、 提交事务,出现异常,数据回滚。

此时出现了数据库没有这条数据,但是二级缓存有这条数据的情况。所以MyBatis 的二级缓存需要跟事务关联起来。

疑问:为什么一级缓存不这么做?

因为一个session就是一个事务,事务回滚,会话就结束了,缓存也清空了,不存 在读到一级缓存中脏数据的情况。二级缓存是跨session的,也就是跨事务的,才有可 能出现对同一个方法的不同事务访问。

1 )写入二级缓存

代码语言:javascript
复制
tcm.putObject(cache, key, list);

从 map中拿出TransactionalCache对象,把 value添加到待提交的Map。 此时缓 存还没有真正地写入。

代码语言:javascript
复制
public void putObject(Object key, Object object) {
	entriesToAddOnCommit.put(key, object);
}

只有事务提交的时候缓存才真正写入(close或者commit最后分析)。

2 ) 获取二级缓存

代码语言:javascript
复制
List<E> list = (List<E>) tcm.getObject(cache, key);

从 map中拿出TransactionalCache对象,这个对象也是对PerpetualCache经过 层层装饰的缓存对象:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0vamVCH-1589355509900)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510212633616.png)]

得到再getObject(), 这个是一个会递归调用的方法,直到到达PerpetualCache, 拿到value。

代码语言:javascript
复制
public Obj ect getObject(Object key) {
	return cache.get(key);
}

8.5.BaseExecutor.query ()

1)清空本地缓存

querystack用于记录查询栈,防止递归查询重复处理缓存。

flushCache=true的时候,会先清理本地缓存(—级缓存):

代码语言:javascript
复制
if (queiyStack == 0 && msJsFlushCacheReqiiiredO) { 			clearLocalCache();
}

如果没有缓存,会从数据库查询:queryFromDatabase

代码语言:javascript
复制
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

如果 LocalCacheScope == STATEMENT,会清理本地缓存。

2)从数据库查询

a)缓存

先在缓存用占位符占位。执行查询后,移除占位符,放入数据。

代码语言:javascript
复制
localCache.putObject(key, EXECUTION_PLACEHOLDER);

b)查询

执行 Executor 的 doQuery; 默认是 SimpleExecutor

代码语言:javascript
复制
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

8.6.SimpleExecutor. doQuery

1)创建 StatementHandler

在 configuration.newStatementHandler(), new—个 StatementHandler, 先 得到 RoutingStatementHandler

RoutingStatementHandler里 面 没 有 任 何 的 实 现 ,是 用 来 创 建 基 本 的 StatementHandler 的。这里会根据 MappedStatement 里面的 statementType 决定 StatementHandler 的 类 型 。 默认是 PREPARED ( STATEMENT. PREPARED、 CALLABLE)

代码语言:javascript
复制
  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());
    }

  }

StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的 ResultSetHandler

这两个对象都是在上面new的时候创建的。

代码语言:javascript
复制
    // 创建了四大对象的其它两大对象 >>
    // 创建这两大对象的时候分别做了什么?
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

这三个对象都是可以被插件拦截的四大对象之一,所以在创建之后都要用拦截器进 行包装的方法。

代码语言:javascript
复制
    // 植入插件逻辑(返回代理对象)
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
代码语言:javascript
复制
    // 植入插件逻辑(返回代理对象)
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
代码语言:javascript
复制
    // 植入插件逻辑(返回代理对象)
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

至此,四大对象的包装已经全部完成。

P S :四大对象还有一个是谁?在什么时候创建的? (Executor)

2 ) 创建 Statement

用 new 出来的 StatementHandler 创建 Statement 对象。

如果有插件包装,会先走到被拦截的业务逻辑。

代码语言:javascript
复制
      // 用Connection创建一个Statement
      stmt = handler.prepare(connection, transaction.getTimeout());

prepareStatement()方法对语句进行预编译,处理参数:

代码语言:javascript
复制
    handler.parameterize(stmt);

这里面会调用parameterHandler设置参数,如果有插件包装,会先走到被拦截的 业务逻辑。

代码语言:javascript
复制
  @Override
  public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
  }

执行的 StatementHandler 的 query方法

RoutingStatementHandler 白勺 query方法 。 delegate 委派,最终执行 PreparedStatementHandler 的 query()方法。

image-20200510214539281
image-20200510214539281

4 ) 执行 PreparedStatement 的 execute方法

后面就是JDBC包中的PreparedStatement的执行了。

  1. ResultSetHandler 处理结果集

如果有插件包装,会先走到被拦截的业务逻辑。

代码语言:javascript
复制
return resultSetHandler.handleResultSets(ps);

问题:怎么把ResultSet转换成List<Object>?

ResultSetHandler 只有一个实现类:DefaultResultSetHandler。也就是执行 DefaultResultSetHandler 的 handleResultSets ()方法。

首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况, —般只有一个结果集。如果下面的这个while循环我们也不用,就是执行一次。

然后会调用handleResultSet方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXDz17MO-1589355509903)(https://hexo-1257630696.cos.ap-singapore.myqcloud.com/img/image-20200510214703265.png)]

9.MyBatis核心对象

image-20200510214737047
image-20200510214737047

参考资料:

1.咕泡学院·MyBatis体系结构与工作原理·青山

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-05-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.MyBatis的工作流程分析
    • 一、解析配置文件
      • 二、提供操作接口
        • 三、执行SQL操作
          • 四、MyBatis主要的工作流程
          • 2.MyBatis架构分层与模块划分(总)
            • 2.1.接口层
              • 2.2.核心处理层
                • 2.3.基础支持层
                • 3.MyBatis缓存详解
                  • 3.1.一级缓存
                    • 3.2.一级缓存验证
                      • 3.3.二级缓存
                        • 3.4.开启二级缓存的方法
                          • 3.5.二级缓存验证
                            • 3.6.第三方缓存做二级缓存
                            • 4.MyBatis源码解读
                              • 4.1.带着问题去看源码
                                • 4.2.看源码的注意事项
                                • 5.配置解析过程
                                  • 5.1.XMLConfigBuilder
                                    • 5.2.propertiesElement()
                                      • 5.3.settingsAsProperties()
                                        • 5.4.loadCustomVfs(settings)
                                          • 5.5.loadCustomLoglmpl(settings)
                                            • 5.6.typeAliasesElement
                                              • 5.7.pluginElement
                                                • 5.8.databaseldProviderElement()
                                                  • 5.9.typeHandlerElement()
                                                    • 5.10.mapperElement()
                                                    • 6.会话创建过程
                                                      • 6.1.创建 Transaction
                                                        • 6.2.创建 Executor
                                                          • 6.3.总结
                                                          • 7.获得Mapper对象
                                                            • 7.1.getMapper()
                                                              • 7.2.MapperProxy如何实现对接口的代理
                                                                • 7.3.总结
                                                                • 8.执行SQL
                                                                  • 8.1.MapperProxy. invoke()
                                                                    • 8.2.MapperMethod. execute()
                                                                      • 8.3.DefaultSqlSession. selectOne()
                                                                        • 8.4.CachingExecutor. query()
                                                                          • 8.5.BaseExecutor.query ()
                                                                            • 8.6.SimpleExecutor. doQuery
                                                                            • 9.MyBatis核心对象
                                                                            相关产品与服务
                                                                            容器服务
                                                                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                                            领券
                                                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档