1.MyBatis應(yīng)用分析與實(shí)踐
2.MyBatis體系結(jié)構(gòu)與工作原理
3.MyBatis插件原理及Spring集成
4.手寫自己的MyBatis框架
本節(jié)目標(biāo):
1、 掌握 MyBatis 的工作流程
2、 掌握 MyBatis 的架構(gòu)分層與模塊劃分
3薪缆、 掌握 MyBatis 緩存機(jī)制
4、 通過閱讀 MyBatis 源碼掌握 MyBatis 底層工作原理與設(shè)計(jì)思想
一,MyBatis 的工作流程分析
在上一篇《應(yīng)用分析與實(shí)踐》里面拣帽,我們學(xué)習(xí)了 MyBatis 的編程式使用的方法疼电,我們?cè)賮砘仡櫼幌?MyBatis 的主要工作流程:
首先在 MyBatis 啟動(dòng)的時(shí)候我們要去解析配置文件,包括全局配置文件和映射器 配置文件减拭,這里面包含了我們?cè)趺纯刂?MyBatis 的行為蔽豺,和我們要對(duì)數(shù)據(jù)庫下達(dá)的指令, 也就是我們的 SQL 信息拧粪。我們會(huì)把它們解析成一個(gè) Configuration 對(duì)象修陡。
接下來就是我們操作數(shù)據(jù)庫的接口,它在應(yīng)用程序和數(shù)據(jù)庫中間可霎,代表我們跟數(shù)據(jù)庫之間的一次連接:這個(gè)就是 SqlSession 對(duì)象魄鸦。
我們要獲得一個(gè)會(huì)話 , 必須有一個(gè)會(huì)話工廠 SqlSessionFactory 癣朗。 SqlSessionFactory 里面又必須包含我們的所有的配置信息拾因,所以我們會(huì)通過一個(gè) Builder 來創(chuàng)建工廠類。
我們知道旷余,MyBatis 是對(duì) JDBC 的封裝绢记,也就是意味著底層一定會(huì)出現(xiàn) JDBC 的一 些核心對(duì)象,比如執(zhí)行 SQL 的 Statement正卧,結(jié)果集 ResultSet庭惜。在 Mybatis 里面, SqlSession 只是提供給應(yīng)用的一個(gè)接口穗酥,還不是 SQL 的真正的執(zhí)行對(duì)象护赊。
我們上次提到了,SqlSession 持有了一個(gè) Executor 對(duì)象砾跃,用來封裝對(duì)數(shù)據(jù)庫的操作骏啰。
在執(zhí)行器 Executor 執(zhí)行 query 或者 update 操作的時(shí)候我們創(chuàng)建一系列的對(duì)象, 來處理參數(shù)抽高、執(zhí)行 SQL判耕、處理結(jié)果集,這里我們把它簡(jiǎn)化成一個(gè)對(duì)象:StatementHandler翘骂, 在閱讀源碼的時(shí)候我們?cè)偃チ私膺€有什么其他的對(duì)象壁熄。
這個(gè)就是 MyBatis 主要的工作流程,如圖:
二碳竟,MyBatis 架構(gòu)分層與模塊劃分
在 MyBatis 的主要工作流程里面草丧,不同的功能是由很多不同的類協(xié)作完成的,它們分布在MyBatis jar 包的不同的 package 里面莹桅。
我們來看一下 MyBatis 的 jar 包(基于 3.5.6)昌执,jar 包結(jié)構(gòu)是這樣的(22 個(gè)包):
大概有 300 多個(gè)類,這樣看起來不夠清楚,不知道什么類在什么環(huán)節(jié)工作懂拾,屬于什么層次煤禽。跟 Spring 一樣,MyBatis 按照功能職責(zé)的不同岖赋,所有的 package 可以分成不同的工作層次檬果。我們可以把 MyBatis 的工作流程類比成餐廳的服務(wù)流程:
- 第一個(gè)是跟客戶打交道的服務(wù)員,它是用來接收程序的工作指令的唐断,我們把它叫做接口層选脊。
- 第二個(gè)是后臺(tái)的廚師,他們根據(jù)客戶的點(diǎn)菜單栗涂,把原材料加工成成品知牌,然后傳到窗口祈争。這一層是真正去操作數(shù)據(jù)的斤程,我們把它叫做核心層。
- 最后就是餐廳也需要有人做后勤(比如清潔菩混、采購忿墅、財(cái)務(wù)),來支持廚師的工作和整個(gè)餐廳的運(yùn)營(yíng)沮峡。我們把它叫做基礎(chǔ)層疚脐。
來看一下這張圖,我們根據(jù)剛才的分層邢疙,和大體的執(zhí)行流程棍弄,做了這么一個(gè)總結(jié)。 當(dāng)然疟游,從不同的角度來描述呼畸,架構(gòu)圖的劃分有所區(qū)別,這張圖畫起來也有很多形式颁虐。我們先從總體上建立一個(gè)印象蛮原。每一層的主要對(duì)象和主要的功能我們也給大家分析一下。
接口層
首先接口層是我們打交道最多的另绩。核心對(duì)象是 SqlSession儒陨,它是上層應(yīng)用和 MyBatis 打交道的橋梁,SqlSession 上定義了非常多的對(duì)數(shù)據(jù)庫的操作方法笋籽。接口層在接收到調(diào)用請(qǐng)求的時(shí)候蹦漠,會(huì)調(diào)用核心處理層的相應(yīng)模塊來完成具體的數(shù)據(jù)庫操作。
核心處理層
接下來是核心處理層车海。既然叫核心處理層津辩,也就是跟數(shù)據(jù)庫操作相關(guān)的動(dòng)作都是在 這一層完成的。核心處理層主要做了這幾件事:
把接口中傳入的參數(shù)解析并且映射成 JDBC 類型;
解析 xml 文件中的 SQL 語句喘沿,包括插入?yún)?shù)闸度,和動(dòng)態(tài) SQL 的生成;
執(zhí)行 SQL 語句蚜印;
處理結(jié)果集莺禁,并映射成 Java 對(duì)象。
插件也屬于核心層窄赋,這是由它的工作方式和攔截的對(duì)象決定的哟冬。
基礎(chǔ)支持層
最后一個(gè)就是基礎(chǔ)支持層∫浯拢基礎(chǔ)支持層主要是一些抽取出來的通用的功能(實(shí)現(xiàn)復(fù)用)浩峡,用來支持核心處理層的功能。比如數(shù)據(jù)源错敢、緩存翰灾、日志、xml 解析稚茅、反射纸淮、IO、 事務(wù)等等這些功能亚享。
這個(gè)就是 MyBatis 的主要工作流程和架構(gòu)分層咽块。接下來我們來學(xué)習(xí)一下基礎(chǔ)層里面 的一個(gè)主要模塊,緩存欺税。我們一起來了解一下 MyBatis 一級(jí)緩存和二級(jí)緩存的區(qū)別侈沪,和 它們的工作方式,以及使用過程里面有什么注意事項(xiàng)晚凿。
三亭罪,MyBatis 緩存詳解
cache 緩存
緩存是一般的 ORM 框架都會(huì)提供的功能,目的就是提升查詢的效率和減少數(shù)據(jù)庫的壓力晃虫。跟 Hibernate 一樣皆撩,MyBatis 也有一級(jí)緩存和二級(jí)緩存,并且預(yù)留了集成第三方緩存的接口哲银。
緩存體系結(jié)構(gòu)
MyBatis 跟緩存相關(guān)的類都在 cache 包里面扛吞,其中有一個(gè) Cache 接口,只有一個(gè)默認(rèn)的實(shí)現(xiàn)類 PerpetualCache荆责,它是用 HashMap 實(shí)現(xiàn)的滥比。
除此之外,還有很多的裝飾器做院,通過這些裝飾器可以額外實(shí)現(xiàn)很多的功能:回收策 略盲泛、日志記錄濒持、定時(shí)刷新等等:
“裝飾者模式(Decorator Pattern)是指在不改變?cè)袑?duì)象的基礎(chǔ)之上,將功能附加到對(duì)象上寺滚,提供了比繼承更有彈 性的替代方案(擴(kuò)展原有對(duì)象的功能)柑营。”
但是無論怎么裝飾村视,經(jīng)過多少層裝飾官套,最后使用的還是基本的實(shí)現(xiàn)類(默認(rèn) PerpetualCache)。
所有的緩存實(shí)現(xiàn)類總體上可分為三類:基本緩存蚁孔、淘汰算法緩存奶赔、裝飾器緩存:
緩存實(shí)現(xiàn)類 | 描述 | 作用 | 裝飾條件 |
---|---|---|---|
基本緩存 | 緩存基本實(shí)現(xiàn)類 | 默認(rèn)是 PerpetualCache,也可以自定義比如 RedisCache杠氢、EhCache 等站刑,具備基本功能的緩存類 | 無 |
LruCache | LRU 策略的緩存 | 當(dāng)緩存到達(dá)上限時(shí)候,刪除最近最少使用的緩存 (Least Recently Use) | eviction="LRU"(默 認(rèn)) |
FifoCache | FIFO 策略的緩存 | 當(dāng)緩存到達(dá)上限時(shí)候鼻百,刪除最先入隊(duì)的緩存 | eviction="FIFO" |
SoftCache WeakCache | 帶清理策略的緩存 | 通過 JVM 的軟引用和弱引用來實(shí)現(xiàn)緩存绞旅,當(dāng) JVM 內(nèi)存不足時(shí),會(huì)自動(dòng)清理掉這些緩存愕宋,基于 SoftReference 和 WeakReference | eviction="SOFT" eviction="WEAK" |
LoggingCache | 帶日志功能的緩存 | 比如:輸出緩存命中率 | 基本 |
SynchronizedCache | 同步緩存 | 基于 synchronized 關(guān)鍵字實(shí)現(xiàn)玻靡,解決并發(fā)問題 | 基本 |
BlockingCache | 阻塞緩存 | 通過在 get/put 方式中加鎖结榄,保證只有一個(gè)線程操作緩存中贝,基于 Java 重入鎖實(shí)現(xiàn) | blocking=true |
SerializedCache | 支持序列化的緩存 | 將對(duì)象序列化以后存到緩存中,取出時(shí)反序列化 | readOnly=false(默 認(rèn)) |
ScheduledCache | 定時(shí)調(diào)度的緩存 | 在進(jìn)行 get/put/remove/getSize 等操作前臼朗,判斷緩存時(shí)間是否超過了設(shè)置的最長(zhǎng)緩存時(shí)間(默認(rèn)是 一小時(shí))邻寿,如果是則清空緩存--即每隔一段時(shí)間清 空一次緩存 | flushInterval 不為空 |
TransactionalCache | 事務(wù)緩存 | 在二級(jí)緩存中使用,可一次存入多個(gè)緩存视哑,移除多 個(gè)緩存 | 在 TransactionalCach eManager 中用 Map 維護(hù)對(duì)應(yīng)關(guān)系 |
思考:緩存對(duì)象在什么時(shí)候創(chuàng)建绣否?什么情況下被裝飾? 我們要弄清楚這個(gè)問題挡毅,就必須要知道 MyBatis 的一級(jí)緩存和二級(jí)緩存的工作位置和工作方式的區(qū)別蒜撮。
一級(jí)緩存
一級(jí)緩存(本地緩存)介紹
一級(jí)緩存也叫本地緩存,MyBatis 的一級(jí)緩存是在會(huì)話(SqlSession)層面進(jìn)行緩存的跪呈。MyBatis 的一級(jí)緩存是默認(rèn)開啟的段磨,不需要任何的配置。
首先我們必須去弄清楚一個(gè)問題耗绿,在 MyBatis 執(zhí)行的流程里面苹支,涉及到這么多的對(duì)象,那么緩存 PerpetualCache 應(yīng)該放在哪個(gè)對(duì)象里面去維護(hù)误阻?如果要在同一個(gè)會(huì)話里面共享一級(jí)緩存债蜜,這個(gè)對(duì)象肯定是在 SqlSession 里面創(chuàng)建的晴埂,作為SqlSession的一個(gè)屬 性。
DefaultSqlSession里面只有兩個(gè)屬性寻定,Configuration是全局的儒洛,所以緩存只可能 放在 Executor 里面維護(hù)——SimpleExecutor/ReuseExecutor/BatchExecutor的父類BaseExecutor的構(gòu)造函數(shù)中持有了 PerpetualCache。
在同一個(gè)會(huì)話里面狼速,多次執(zhí)行相同的 SQL 語句晶丘,會(huì)直接從內(nèi)存取到緩存的結(jié)果,不會(huì)再發(fā)送 SQL 到數(shù)據(jù)庫唐含。但是不同的會(huì)話里面浅浮,即使執(zhí)行的 SQL 一模一樣(通過一個(gè) Mapper 的同一個(gè)方法的相同參數(shù)調(diào)用),也不能使用到一級(jí)緩存捷枯。
接下來我們來驗(yàn)證一下滚秩,MyBatis 的一級(jí)緩存到底是不是只能在一個(gè)會(huì)話里面共享, 以及跨會(huì)話(不同 session)操作相同的數(shù)據(jù)會(huì)產(chǎn)生什么問淮捆。
一級(jí)緩存驗(yàn)證
(注意演示一級(jí)緩存需要先關(guān)閉二級(jí)緩存郁油, localCacheScope 設(shè)置為 SESSION)
判斷是否命中緩存:如果再次發(fā)送 SQL 到數(shù)據(jù)庫執(zhí)行,說明沒有命中緩存攀痊;如果直接打印對(duì)象桐腌,說明是從內(nèi)存緩存中取到了結(jié)果。
-
在同一個(gè) session 中共享
/** * 測(cè)試一級(jí)緩存需要先關(guān)閉二級(jí)緩存苟径,localCacheScope設(shè)置為SESSION * @throws IOException */ @Test public void testCache() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); try { BlogMapper mapper0 = session1.getMapper(BlogMapper.class); BlogMapper mapper1 = session1.getMapper(BlogMapper.class); Blog blog = mapper0.selectBlogById(1); System.out.println(blog); System.out.println("第二次查詢案站,相同會(huì)話,獲取到緩存了嗎棘街?"); System.out.println(mapper1.selectBlogById(1)); System.out.println("第三次查詢蟆盐,不同會(huì)話,獲取到緩存了嗎遭殉?"); BlogMapper mapper2 = session2.getMapper(BlogMapper.class); System.out.println(mapper2.selectBlogById(1)); } finally { session1.close(); } }
-
不同 session 不能共享
SqlSession session1 = sqlSessionFactory.openSession(); BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper.selectBlog(1));
PS:一級(jí)緩存在 BaseExecutor 的 query()——queryFromDatabase()中存入石挂。在 queryFromDatabase()之前會(huì) get()。
-
同一個(gè)會(huì)話中险污,update(包括 delete)會(huì)導(dǎo)致一級(jí)緩存被清空
/** * 一級(jí)緩存失效 * @throws IOException */ @Test public void testCacheInvalid() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { BlogMapper mapper = session.getMapper(BlogMapper.class); System.out.println(mapper.selectBlogById(1)); Blog blog = new Blog(); blog.setBid(1); blog.setName("2021年7月24日14:39:58"); mapper.updateByPrimaryKey(blog); session.commit(); // 相同會(huì)話執(zhí)行了更新操作痹愚,緩存是否被清空? System.out.println("在執(zhí)行更新操作之后蛔糯,是否命中緩存拯腮?"); System.out.println(mapper.selectBlogById(1)); } finally { session.close(); } }
一級(jí)緩存是在 BaseExecutor 中的 update()方法中調(diào)用 clearLocalCache()清空的(無條件),query 中會(huì)判斷渤闷。如果跨會(huì)話疾瓮,會(huì)出現(xiàn)什么問題?
-
其他會(huì)話更新了數(shù)據(jù)飒箭,導(dǎo)致讀取到臟數(shù)據(jù)(一級(jí)緩存不能跨會(huì)話共享)狼电。
/** * 因?yàn)榫彺娌荒芸鐣?huì)話共享蜒灰,導(dǎo)致臟數(shù)據(jù)出現(xiàn) * @throws IOException */ @Test public void testDirtyRead() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); try { BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper1.selectBlogById(1)); // 會(huì)話2更新了數(shù)據(jù),會(huì)話2的一級(jí)緩存更新 Blog blog = new Blog(); blog.setBid(1); blog.setName("after modified 112233445566"); BlogMapper mapper2 = session2.getMapper(BlogMapper.class); mapper2.updateByPrimaryKey(blog); session2.commit(); // 其他會(huì)話更新了數(shù)據(jù)肩碟,本會(huì)話的一級(jí)緩存還在么强窖? System.out.println("會(huì)話1查到最新的數(shù)據(jù)了嗎?"); System.out.println(mapper1.selectBlogById(1)); } finally { session1.close(); session2.close(); } }
一級(jí)緩存的不足
使用一級(jí)緩存的時(shí)候削祈,因?yàn)榫彺娌荒芸鐣?huì)話共享翅溺,不同的會(huì)話之間對(duì)于相同的數(shù)據(jù)可能有不一樣的緩存。在有多個(gè)會(huì)話或者分布式環(huán)境下髓抑,會(huì)存在臟數(shù)據(jù)的問題咙崎。如果要解決這個(gè)問題,就要用到二級(jí)緩存吨拍。
【思考】一級(jí)緩存怎么命中褪猛?CacheKey 怎么構(gòu)成?
【思考】一級(jí)緩存是默認(rèn)開啟的羹饰,怎么關(guān)閉一級(jí)緩存伊滋?
二級(jí)緩存
二級(jí)緩存介紹
二級(jí)緩存是用來解決一級(jí)緩存不能跨會(huì)話共享的問題的,范圍是 namespace 級(jí)別的队秩,可以被多個(gè) SqlSession 共享(只要是同一個(gè)接口里面的相同方法笑旺,都可以共享), 生命周期和應(yīng)用同步馍资。
思考一個(gè)問題:如果開啟了二級(jí)緩存筒主,二級(jí)緩存應(yīng)該是工作在一級(jí)緩存之前,還是 在一級(jí)緩存之后呢迷帜?二級(jí)緩存是在哪里維護(hù)的呢物舒?
作為一個(gè)作用范圍更廣的緩存色洞,它肯定是在 SqlSession 的外層戏锹,否則不可能被多個(gè) SqlSession 共享。而一級(jí)緩存是在 SqlSession 內(nèi)部的火诸,所以第一個(gè)問題锦针,肯定是工作在一級(jí)緩存之前,也就是只有取不到二級(jí)緩存的情況下才到一個(gè)會(huì)話中去取一級(jí)緩存置蜀。
第二個(gè)問題奈搜,二級(jí)緩存放在哪個(gè)對(duì)象中維護(hù)呢? 要跨會(huì)話共享的話盯荤,SqlSession 本身和它里面的 BaseExecutor 已經(jīng)滿足不了需求了馋吗,那我們應(yīng)該在 BaseExecutor 之外創(chuàng)建一個(gè)對(duì)象。
實(shí)際上 MyBatis 用了一個(gè)裝飾器的類來維護(hù)秋秤,就是 CachingExecutor宏粤。如果啟用了 二級(jí)緩存脚翘,MyBatis 在創(chuàng)建 Executor 對(duì)象的時(shí)候會(huì)對(duì) Executor 進(jìn)行裝飾毡熏。
CachingExecutor 對(duì)于查詢請(qǐng)求翩瓜,會(huì)判斷二級(jí)緩存是否有緩存結(jié)果,如果有就直接返回乘盼,如果沒有委派交給真正的查詢器 Executor 實(shí)現(xiàn)類崇堰,比如 SimpleExecutor 來執(zhí)行查詢沃于,再走到一級(jí)緩存的流程。最后會(huì)把結(jié)果緩存起來海诲,并且返回給用戶繁莹。
一級(jí)緩存是默認(rèn)開啟的,那二級(jí)緩存怎么開啟呢特幔?
開啟二級(jí)緩存的方法
第一步:在 mybatis-config.xml 中配置了(可以不配置,默認(rèn)是 true):
<setting name="cacheEnabled" value="true"/>
只要沒有顯式地設(shè)置 cacheEnabled=false敬辣,都會(huì)用 CachingExecutor 裝飾基本的執(zhí)行器雪标。
第二步:在 Mapper.xml 中配置標(biāo)簽。
<!-- 聲明這個(gè) namespace 使用二級(jí)緩存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!—最多緩存對(duì)象個(gè)數(shù)溉跃,默認(rèn) 1024-->
eviction="LRU" <!—回收策略-->
flushInterval="120000" <!—自動(dòng)刷新時(shí)間 ms村刨,未配置時(shí)只有調(diào)用時(shí)刷新-->
readOnly="false"/> <!—默認(rèn)是 false(安全),改為 true 可讀寫時(shí)撰茎,對(duì)象必須支持序列化 -->
cache 屬性詳解:
屬性 | 含義 | 取值 |
---|---|---|
type | 緩存實(shí)現(xiàn)類 | 需要實(shí)現(xiàn) Cache 接口嵌牺,默認(rèn)是 PerpetualCache |
size | 最多緩存對(duì)象個(gè)數(shù) | 默認(rèn) 1024 |
eviction | 回收策略(緩存淘汰算法) | LRU – 最近最少使用的:移除最長(zhǎng)時(shí)間不被使用的對(duì)象(默認(rèn))。<br />FIFO – 先進(jìn)先出:按對(duì)象進(jìn)入緩存的順序來移除它們龄糊。 <br />SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象逆粹。<br />WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象。 |
flushInterval | 定時(shí)自動(dòng)清空緩存間隔 | 自動(dòng)刷新時(shí)間炫惩,單位 ms僻弹,未配置時(shí)只有調(diào)用時(shí)刷新 |
readOnly | 是否只讀 | true:只讀緩存;會(huì)給所有調(diào)用者返回緩存對(duì)象的相同實(shí)例他嚷。因此這些對(duì)象 不能被修改蹋绽。這提供了很重要的性能優(yōu)勢(shì)。 <br />false:讀寫緩存筋蓖;會(huì)返回緩存對(duì)象的拷貝(通過序列化)卸耘,不會(huì)共享。這 會(huì)慢一些粘咖,但是安全蚣抗,因此默認(rèn)是 false。 改為 false 可讀寫時(shí)瓮下,對(duì)象必須支持序列化翰铡。 |
blocking | 是否使用可重入鎖實(shí)現(xiàn)緩存的并發(fā)控制 | true设哗,會(huì)使用 BlockingCache 對(duì) Cache 進(jìn)行裝飾 默認(rèn) false |
Mapper.xml 配置了之后,select()會(huì)被緩存两蟀。update()网梢、delete()、insert() 會(huì)刷新緩存赂毯。
思考:如果 cacheEnabled=true战虏,Mapper.xml 沒有配置標(biāo)簽,還有二級(jí)緩存嗎党涕? 還會(huì)出現(xiàn) CachingExecutor 包裝對(duì)象嗎烦感?
只要 cacheEnabled=true 基本執(zhí)行器就會(huì)被裝飾。有沒有配置膛堤,決定了在啟動(dòng)的時(shí)候會(huì)不會(huì)創(chuàng)建這個(gè) mapper 的 Cache 對(duì)象手趣,最終會(huì)影響到 CachingExecutor query 方法里面的判斷:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果某些查詢方法對(duì)數(shù)據(jù)的實(shí)時(shí)性要求很高,不需要二級(jí)緩存肥荔,怎么辦绿渣?
我們可以在單個(gè) Statement ID 上顯式關(guān)閉二級(jí)緩存(默認(rèn)是 true)
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
了解了二級(jí)緩存的工作位置和開啟關(guān)閉的方法之后,我們也來驗(yàn)證一下二級(jí)緩存燕耿。
二級(jí)緩存驗(yàn)證
(驗(yàn)證二級(jí)緩存需要先開啟二級(jí)緩存)
-
事務(wù)不提交中符,二級(jí)緩存不存在
/** * 測(cè)試二級(jí)緩存一定要打開二級(jí)緩存開關(guān) * @throws IOException */ @Test public void testCache() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); try { BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper1.selectBlogById(1)); // 事務(wù)不提交的情況下,二級(jí)緩存會(huì)寫入嗎誉帅? //session1.commit(); System.out.println("第二次查詢"); BlogMapper mapper2 = session2.getMapper(BlogMapper.class); System.out.println(mapper2.selectBlogById(1)); } finally { session1.close(); } }
思考:為什么事務(wù)不提交淀散,二級(jí)緩存不生效?
因?yàn)槎?jí)緩存使用 TransactionalCacheManager(TCM)來管理蚜锨,最后又調(diào)用了 TransactionalCache 的getObject()档插、putObject和 commit()方法,TransactionalCache 里面又持有了真正的 Cache 對(duì)象亚再,比如是經(jīng)過層層裝飾的 PerpetualCache郭膛。
在putObject 的時(shí)候,只是添加到了 entriesToAddOnCommit 里面针余,只有它的 commit()方法被調(diào)用的時(shí)候才會(huì)調(diào)用 flushPendingEntries()真正寫入緩存饲鄙。它就是在 DefaultSqlSession 調(diào)用 commit()的時(shí)候被調(diào)用的。
-
使用不同的 session 和 mapper圆雁,驗(yàn)證二級(jí)緩存可以跨 session 存在(取消以上 commit()的注釋)
/** * 測(cè)試二級(jí)緩存一定要打開二級(jí)緩存開關(guān) * @throws IOException */ @Test public void testCache() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); try { BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper1.selectBlogById(1)); // 事務(wù)不提交的情況下,二級(jí)緩存會(huì)寫入嗎帆谍? session1.commit(); System.out.println("第二次查詢"); BlogMapper mapper2 = session2.getMapper(BlogMapper.class); System.out.println(mapper2.selectBlogById(1)); } finally { session1.close(); } }
-
在其他的 session 中執(zhí)行增刪改操作伪朽,驗(yàn)證緩存會(huì)被刷新
Blog blog = new Blog(); blog.setBid(1); blog.setName("357"); mapper3.updateByPrimaryKey(blog); session3.commit(); // 執(zhí)行了更新操作,二級(jí)緩存失效汛蝙,再次發(fā)送 SQL 查詢 System.out.println(mapper2.selectBlogById(1));
思考:為什么增刪改操作會(huì)清空緩存烈涮?
在CachingExecutor 的 update()方法里面會(huì)調(diào)用 flushCacheIfRequired(ms)朴肺, isFlushCacheRequired 就是從標(biāo)簽里面渠道的 flushCache 的值。而增刪改操作的 flushCache 屬性默認(rèn)為 true
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
什么時(shí)候開啟二級(jí)緩存坚洽?
一級(jí)緩存默認(rèn)是打開的戈稿,二級(jí)緩存需要配置才可以開啟。那么我們必須思考一個(gè)問題讶舰,在什么情況下才有必要去開啟二級(jí)緩存鞍盗?
1、因?yàn)樗械脑鰟h改都會(huì)刷新二級(jí)緩存跳昼,導(dǎo)致二級(jí)緩存失效般甲,所以適合在查詢?yōu)橹鞯膽?yīng)用中使用,比如歷史交易鹅颊、歷史訂單的查詢敷存。否則緩存就失去了意義。
2堪伍、如果多個(gè) namespace 中有針對(duì)于同一個(gè)表的操作锚烦,比如 Blog 表,如果在一個(gè) namespace 中刷新了緩存帝雇,另一個(gè) namespace 中沒有刷新挽牢,就會(huì)出現(xiàn)讀到臟數(shù)據(jù)的情況。所以摊求,推薦在一個(gè) Mapper 里面只操作單表的情況使用禽拔。
思考:如果要讓多個(gè) namespace 共享一個(gè)二級(jí)緩存,應(yīng)該怎么做室叉?
跨 namespace 的緩存共享的問題睹栖,可以使用<cache-ref>來解決:
<cache-ref namespace="com.javacoo.crud.dao.DepartmentMapper" />
cache-ref 代表引用別的命名空間的 Cache 配置,兩個(gè)命名空間的操作使用的是同 一個(gè)Cache茧痕。在關(guān)聯(lián)的表比較少野来,或者按照業(yè)務(wù)可以對(duì)表進(jìn)行分組的時(shí)候可以使用。 注意:在這種情況下踪旷,多個(gè) Mapper 的操作都會(huì)引起緩存刷新曼氛,緩存的意義已經(jīng)不大了。
第三方緩存做二級(jí)緩存
除了 MyBatis 自帶的二級(jí)緩存之外令野,我們也可以通過實(shí)現(xiàn) Cache 接口來自定義二級(jí)緩存舀患。
MyBatis 官方提供了一些第三方緩存集成方式,比如 ehcache 和 redis:
https://github.com/mybatis/redis-cache
pom 文件引入依賴:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</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
當(dāng)然聊浅,我們也可以使用獨(dú)立的緩存服務(wù),不使用 MyBatis 自帶的二級(jí)緩存。
四低匙,MyBatis 源碼解讀
帶著問題去看源碼旷痕,分析源碼,我們還是從編程式的 demo 入手顽冶。Spring 的集成我們會(huì)在后面講到
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
把文件讀取成流的這一步我們就省略了欺抗。所以下面我們分成四步來分析。
第一步强重,我們通過建造者模式創(chuàng)建一個(gè)工廠類绞呈,配置文件的解析就是在這一步完成 的,包括 mybatis-config.xml 和 Mapper 適配器文件竿屹。
問題:解析的時(shí)候怎么解析的报强,做了什么,產(chǎn)生了什么對(duì)象拱燃,結(jié)果存放到了哪里秉溉。 解析的結(jié)果決定著我們后面有什么對(duì)象可以使用,和到哪里去取?
第二步碗誉,通過 SqlSessionFactory 創(chuàng)建一個(gè) SqlSession召嘶。
問題:SqlSession 是用來操作數(shù)據(jù)庫的,返回了什么實(shí)現(xiàn)類哮缺,除了 SqlSession弄跌,還創(chuàng)建了什么對(duì)象,創(chuàng)建了什么環(huán)境尝苇?
第三步铛只,獲得一個(gè) Mapper 對(duì)象。
問題:Mapper是一個(gè)接口糠溜,沒有實(shí)現(xiàn)類淳玩,是不能被實(shí)例化的,那獲取到的這個(gè) Mapper 對(duì)象是什么對(duì)象非竿?為什么要從 SqlSession 里面去獲韧勺拧?為什么傳進(jìn)去一個(gè)接口红柱,然后還要用接口類型來接收承匣?
第四步,調(diào)用接口方法锤悄。
問題:我們的接口沒有創(chuàng)建實(shí)現(xiàn)類韧骗,為什么可以調(diào)用它的方法?那它調(diào)用的是什么方法铁蹈?它又是根據(jù)什么找到我們要執(zhí)行的 SQL 的宽闲?也就是接口方法怎么和 XML 映射器里面的 StatementID 關(guān)聯(lián)起來的众眨?
此外握牧,我們的方法參數(shù)是怎么轉(zhuǎn)換成 SQL 參數(shù)的容诬?獲取到的結(jié)果集是怎么轉(zhuǎn)換成對(duì)象的?
接下來我們就會(huì)詳細(xì)分析每一步的流程沿腰,包括里面有哪些核心的對(duì)象和關(guān)鍵的方法览徒。
1.配置解析過程
首先我們要清楚的是配置解析的過程全部只解析了兩種文件 。 一 個(gè)是 mybatis-config.xml 全局配置文件颂龙。另外就是可能有很多個(gè)的 Mapper.xml 文件习蓬,也包括在 Mapper 接口類上面定義的注解。
我們從 mybatis-config.xml 開始措嵌。在第一節(jié)的時(shí)候我們已經(jīng)分析了核心配置了躲叼, 大概明白了 MyBatis 有哪些配置項(xiàng),和這些配置項(xiàng)的大致含義企巢。這里我們?cè)倬唧w看一下 這里面的標(biāo)簽都是怎么解析的枫慷,解析的時(shí)候做了什么。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先我們 new 了一個(gè) SqlSessionFactoryBuilder浪规,非常明顯的建造者模式或听,它里面定義了很多個(gè) build 方法的重載,最終返回的是一個(gè) SqlSessionFactory 對(duì)象(單例模式)笋婿。我們點(diǎn)進(jìn)去 build 方法誉裆。 這里面創(chuàng)建了一個(gè) XMLConfigBuilder 對(duì)象(Configuration 對(duì)象也是這個(gè)時(shí)候創(chuàng)建的)。
XMLConfigBuilder
XMLConfigBuilder 是抽象類 BaseBuilder 的一個(gè)子類缸濒,專門用來解析全局配置文 件足丢,針對(duì)不同的構(gòu)建目標(biāo)還有其他的一些子類,比如: XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增刪改查標(biāo)簽
根據(jù)我們解析的文件流庇配,這里后面兩個(gè)參數(shù)都是空的斩跌,創(chuàng)建了一個(gè) parser。這里有兩步讨永,第一步是調(diào)用 parser 的 parse()方法滔驶,它會(huì)返回一個(gè) Configuration 類。
之前我們說過卿闹,也就是配置文件里面所有的信息都會(huì)放在 Configuration 里面揭糕。 Configuration 類里面有很多的屬性,有很多是跟 config 里面的標(biāo)簽直接對(duì)應(yīng)的锻霎。
我們先看一下 parse()方法著角。
首先會(huì)檢查是不是已經(jīng)解析過,也就是說在應(yīng)用的生命周期里面旋恼,config 配置文件只需要解析一次吏口,生成的 Configuration 對(duì)象也會(huì)存在應(yīng)用的整個(gè)生命周期中。接下來 就是 parseConfiguration 方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
這下面有十幾個(gè)方法,對(duì)應(yīng)著 config 文件里面的所有一級(jí)標(biāo)簽产徊。 問題:MyBatis 全局配置文件的順序可以顛倒嗎昂勒?
propertiesElement()
第一個(gè)是解析標(biāo)簽,讀取我們引入的外部配置文件舟铜。這里面又有兩種類型戈盈,一種是放在 resource 目錄下的,是相對(duì)路徑谆刨,一種是寫的絕對(duì)路徑的塘娶。解析的最 終結(jié)果就是我們會(huì)把所有的配置信息放到名為 defaults 的 Properties 對(duì)象里面,最后把 XPathParser 和 Configuration 的 Properties 屬性都設(shè)置成我們填充后的 Properties 對(duì)象痊夭。
settingsAsProperties()
第二個(gè)刁岸,我們把標(biāo)簽也解析成了一個(gè) Properties 對(duì)象,對(duì)于標(biāo)簽的子標(biāo)簽的處理在后面她我。
在早期的版本里面解析和設(shè)置都是在后面一起的虹曙,這里先解析成 Properties 對(duì)象是 因?yàn)橄旅娴膬蓚€(gè)方法要用到。
loadCustomVfs(settings)
loadCustomVfs 是獲取 Vitual File System 的自定義實(shí)現(xiàn)類鸦难,比如我們要讀取本地 文件根吁,或者 FTP 遠(yuǎn)程文件的時(shí)候,就可以用到自定義的 VFS 類合蔽。我們根據(jù)標(biāo) 簽里面的標(biāo)簽击敌,生成了一個(gè)抽象類 VFS 的子類,并且賦值到 Configuration 中拴事。
loadCustomLogImpl(settings)
loadCustomLogImpl 是根據(jù)標(biāo)簽獲取日志的實(shí)現(xiàn)類沃斤,我們可以用到很多的日志的方案,包括 LOG4J刃宵,LOG4J2衡瓶,SLF4J 等等。這里生成了一個(gè) Log 接口的實(shí)現(xiàn)類牲证,并且賦值到 Configuration 中
typeAliasesElement()
接下來哮针,我們解析標(biāo)簽,我們?cè)谥v配置的時(shí)候也講過坦袍,它有兩種定義方式十厢,一種是直接定義一個(gè)類的別名,一種就是指定一個(gè)包捂齐,那么這個(gè) package 下面所有的類的名字就會(huì)成為這個(gè)類全路徑的別名蛮放。
類的別名和類的關(guān)系,我們放在一個(gè) TypeAliasRegistry 對(duì)象里面
pluginElement()
接下來就是解析<plugins>標(biāo)簽奠宜,比如 Pagehelper 的翻頁插件包颁,或者我們自定義的插件瞻想。<plugins>標(biāo)簽里面只有<plugin>標(biāo)簽,<plugin>標(biāo)簽里面只有<property>標(biāo)簽娩嚼。
標(biāo)簽解析完以后蘑险,會(huì)生成一個(gè) Interceptor 對(duì)象,并且添加到 Configuration 的 InterceptorChain 屬性里面待锈,它是一個(gè) List漠其。
objectFactoryElement()嘴高、objectWrapperFactoryElement()
接 下 來 的 兩 個(gè) 標(biāo) 簽 是 用 來 實(shí) 例 化 對(duì) 象 用 的 竿音, <objectFactory>和 <objectWrapperFactory>這兩個(gè)標(biāo)簽 , 分別生成 ObjectFactory 拴驮、 ObjectWrapperFactory 對(duì)象春瞬,同樣設(shè)置到 Configuration 的屬性里面。
reflectorFactoryElement()
解析 reflectorFactory 標(biāo)簽套啤,生成 ReflectorFactory 對(duì)象(在官方 3.5.1 的 pdf 文檔里面沒有找到這個(gè)配置)宽气。
settingsElement(settings)
這里就是對(duì)標(biāo)簽里面所有子標(biāo)簽的處理了,前面我們已經(jīng)把子標(biāo)簽全部轉(zhuǎn)換成了 Properties 對(duì)象潜沦,所以在這里處理 Properties 對(duì)象就可以了萄涯。 二級(jí)標(biāo)簽里面有很多的配置,比如二級(jí)緩存唆鸡,延遲加載涝影,自動(dòng)生成主鍵這些。需要注意的是争占,我們之前提到的所有的默認(rèn)值燃逻,都是在這里賦值的。如果說后面我們不知道這個(gè)屬性的值是什么臂痕,也可以到這一步來確認(rèn)一下伯襟。 所有的值,都會(huì)賦值到 Configuration 的屬性里面去握童。
environmentsElement()
這一步是解析<environments>標(biāo)簽姆怪。 我們前面講過,一個(gè) environment 就是對(duì)應(yīng)一個(gè)數(shù)據(jù)源澡绩,所以在這里我們會(huì)根據(jù)配 置的創(chuàng)建一個(gè)事務(wù)工廠稽揭,根據(jù)標(biāo)簽創(chuàng)建一個(gè)數(shù)據(jù)源,最后把這兩個(gè)對(duì)象設(shè)置成 Environment 對(duì)象的屬性英古,放到 Configuration 里面淀衣。 回答了前面的問題:數(shù)據(jù)源工廠和數(shù)據(jù)源在哪里創(chuàng)建。 先記下這個(gè)問題:數(shù)據(jù)源和事務(wù)工廠在哪里會(huì)用到召调?
databaseIdProviderElement()
解析 databaseIdProvider 標(biāo)簽膨桥,生成 DatabaseIdProvider 對(duì)象(用來支持不同廠 商的數(shù)據(jù)庫)蛮浑。
typeHandlerElement()
跟 TypeAlias 一樣,TypeHandler 有兩種配置方式只嚣,一種是單獨(dú)配置一個(gè)類沮稚,一種是指定一個(gè) package。最后我們得到的是 JavaType 和 JdbcType册舞,以及用來做相互映射 的 TypeHandler 之間的映射關(guān)系蕴掏。 最后存放在 TypeHandlerRegistry 對(duì)象里面。
問題:這種三個(gè)對(duì)象(Java 類型调鲸,JDBC 類型盛杰,Handler)的關(guān)系怎么映射?(Map 里面再放一個(gè) Map)
mapperElement()
http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers
1)判斷
最后就是<mappers>標(biāo)簽的解析藐石。
掃描類型 | 含義 |
---|---|
resource | 相對(duì)路徑 |
url | 絕對(duì)路徑 |
package | 包 |
class | 單個(gè)接口 |
首先會(huì)判斷是不是接口即供,只有接口才解析;然后判斷是不是已經(jīng)注冊(cè)了于微,單個(gè) Mapper 重復(fù)注冊(cè)會(huì)拋出異常逗嫡。
2)注冊(cè)
XMLMapperBuilder.parse()方法,是對(duì) Mapper 映射器的解析株依。里面有兩個(gè)方法:
configurationElement()—— 解 析 所 有 的 子 標(biāo) 簽 驱证, 其 中 buildStatementFromContext()最終獲得 MappedStatement 對(duì)象。
bindMapperForNamespace()——把 namespace(接口類型)和工廠類綁定起來恋腕。
無論是按 package 掃描抹锄,還是按接口掃描,最后都會(huì)調(diào)用到 MapperRegistry 的 addMapper()方法吗坚。
MapperRegistry 里面維護(hù)的其實(shí)是一個(gè) Map 容器祈远,存儲(chǔ)接口和代理工廠的映射關(guān) 系。
問題:為什么要放一個(gè)代理工廠呢商源?代理工廠用來干什么车份?
3)處理注解
除了映射器文件,在這里也會(huì)去解析 Mapper 接口方法上的注解牡彻。在 addMapper() 方法里面創(chuàng)建了一個(gè) MapperAnnotationBuilder扫沼,我們點(diǎn)進(jìn)去看一下 parse()方法。
parseCache() 和 parseCacheRef() 方 法 其 實(shí) 是 對(duì) @CacheNamespace 和 @CacheNamespaceRef 這兩個(gè)注解的處理庄吼。
parseStatement()方法里面的各種 getAnnotation()缎除,都是對(duì)注解的解析,比如 @Options总寻,@SelectKey器罐,@ResultMap 等等。
最后同樣會(huì)解析成 MappedStatement 對(duì)象渐行,也就是說在 XML 中配置轰坊,和使用注 解配置铸董,最后起到一樣的效果。
4)收尾
如果注冊(cè)沒有完成肴沫,還要從 Map 里面 remove 掉粟害。
finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
最后,MapperRegistry 也會(huì)放到 Configuration 里面去颤芬。
第二步是調(diào)用另一個(gè) build()方法悲幅,返回 DefaultSqlSessionFactory。
總結(jié)
在這一步站蝠,我們主要完成了 config 配置文件汰具、Mapper 文件、Mapper 接口上的注 解的解析沉衣。
我們得到了一個(gè)最重要的對(duì)象 Configuration郁副,這里面存放了全部的配置信息,它在屬性里面還有各種各樣的容器豌习。最后,返回了一個(gè) DefaultSqlSessionFactory拔疚,里面持有了 Configuration 的實(shí)例肥隆。
2.會(huì)話創(chuàng)建過程
這是第二步,我們跟數(shù)據(jù)庫的每一次連接稚失,都需要?jiǎng)?chuàng)建一個(gè)會(huì)話栋艳,我們用 openSession()方法來創(chuàng)建。
DefaultSqlSessionFactory —— openSessionFromDataSource()
這個(gè)會(huì)話里面句各,需要包含一個(gè) Executor 用來執(zhí)行 SQL吸占。Executor 又要指定事務(wù)類 型和執(zhí)行器的類型。
所以我們會(huì)先從 Configuration 里面拿到 Enviroment凿宾,Enviroment 里面就有事務(wù)工廠矾屯。
-
創(chuàng)建 Transaction
屬性 產(chǎn)生工廠類 產(chǎn)生事務(wù) JDBC JdbcTransactionFactory JdbcTransaction MANAGED ManagedTransactionFactory ManagedTransaction
如果配置的是 JDBC,則會(huì)使用 Connection 對(duì)象的 commit()初厚、rollback()件蚕、close() 管理事務(wù)。
如果配置成 MANAGED产禾,會(huì)把事務(wù)交給容器來管理排作,比如 JBOSS,Weblogic亚情。因 為我們跑的是本地程序妄痪,如果配置成 MANAGE 不會(huì)有任何事務(wù)。
如果是 Spring + MyBatis 楞件, 則沒有必要配置 衫生, 因?yàn)槲覀儠?huì)直接在 applicationContext.xml 里面配置數(shù)據(jù)源和事務(wù)管理器僧著,覆蓋 MyBatis 的配置。
- 創(chuàng)建 Executor
我們知道障簿,Executor 的基本類型有三種:SIMPLE盹愚、BATCH、REUSE站故,默認(rèn)是 SIMPLE (settingsElement()讀取默認(rèn)值)皆怕,他們都繼承了抽象類 BaseExecutor。為什么要讓抽象類實(shí)現(xiàn)接口西篓,然后讓具體實(shí)現(xiàn)類繼承抽象類愈腾?(模板方法模式)
時(shí)序圖:
問題:三種類型的區(qū)別(通過 update()方法對(duì)比)?
SimpleExecutor:每執(zhí)行一次 update 或 select岂津,就開啟一個(gè) Statement 對(duì)象虱黄,用 完立刻關(guān)閉 Statement對(duì)象。
ReuseExecutor:執(zhí)行 update 或 select吮成,以 sql 作為 key 查找 Statement 對(duì)象橱乱, 存在就使用,不存在就創(chuàng)建粱甫,用完后泳叠,不關(guān)閉 Statement 對(duì)象,而是放置于 Map 內(nèi)茶宵, 供下一次使用危纫。簡(jiǎn)言之,就是重復(fù)使用 Statement 對(duì)象乌庶。
BatchExecutor:執(zhí)行 update(沒有 select种蝶,JDBC 批處理不支持 select),將所 有 sql 都添加到批處理中(addBatch())瞒大,等待統(tǒng)一執(zhí)行(executeBatch())螃征,它緩存 了多個(gè) Statement 對(duì)象,每個(gè) Statement 對(duì)象都是 addBatch()完畢后糠赦,等待逐一執(zhí)行 executeBatch()批處理会傲。與 JDBC 批處理相同。
如果配置了 cacheEnabled=ture拙泽,會(huì)用裝飾器模式對(duì) executor 進(jìn)行包裝:new CachingExecutor(executor)淌山。包裝完畢后,會(huì)執(zhí)行:
executor = (Executor) interceptorChain.pluginAll(executor);
此處會(huì)對(duì) executor 進(jìn)行包裝顾瞻。
回答了前面的問題:數(shù)據(jù)源和事務(wù)工廠在哪里會(huì)用到——?jiǎng)?chuàng)建執(zhí)行器的時(shí)候泼疑。
最終返回 DefaultSqlSession,屬性包括 Configuration荷荤、Executor 對(duì)象退渗。
總結(jié):創(chuàng)建會(huì)話的過程移稳,我們獲得了一個(gè) DefaultSqlSession,里面包含了一個(gè) Executor会油,它是 SQL 的執(zhí)行者个粱。
3.獲得 Mapper 對(duì)象
現(xiàn)在我們已經(jīng)有一個(gè) DefaultSqlSession 了,必須找到 Mapper.xml 里面定義的 Statement ID翻翩,才能執(zhí)行對(duì)應(yīng)的 SQL 語句都许。
找到 Statement ID 有兩種方式:一種是直接調(diào)用 session 的方法,在參數(shù)里面?zhèn)魅?Statement ID嫂冻,這種方式屬于硬編碼胶征,我們沒辦法知道有多少處調(diào)用,修改起來也很麻煩桨仿。
另一個(gè)問題是如果參數(shù)傳入錯(cuò)誤睛低,在編譯階段也是不會(huì)報(bào)錯(cuò)的,不利于預(yù)先發(fā)現(xiàn)問題服傍。
Blog blog = (Blog) session.selectOne("com.javacoo.mapper.BlogMapper.selectBlogById", 1);
所以在 MyBatis 后期的版本提供了第二種方式钱雷,就是定義一個(gè)接口,然后再調(diào)用 Mapper 接口的方法伴嗡。
由于我們的接口名稱跟 Mapper.xml 的 namespace 是對(duì)應(yīng)的急波,接口的方法跟 statement ID 也都是對(duì)應(yīng)的,所以根據(jù)方法就能找到對(duì)應(yīng)的要執(zhí)行的 SQL瘪校。
BlogMapper mapper = session.getMapper(BlogMapper.class);
在這里我們主要研究一下 Mapper 對(duì)象是怎么獲得的,它的本質(zhì)是什么名段。
DefaultSqlSession 的 getMapper()方法阱扬,調(diào)用了 Configuration 的 getMapper() 方法。
configuration.<T>getMapper()
Configuration 的 getMapper()方法伸辟,又調(diào)用了 MapperRegistry 的 getMapper() 方法麻惶。
mapperRegistry.getMapper()
我們知道,在解析 mapper 標(biāo)簽和 Mapper.xml 的時(shí)候已經(jīng)把接口類型和類型對(duì)應(yīng) 的 MapperProxyFactory 放到了一個(gè) Map 中信夫。獲取 Mapper 代理對(duì)象窃蹋,實(shí)際上是從 Map 中獲取對(duì)應(yīng)的工廠類后,調(diào)用以下方法創(chuàng)建對(duì)象:
MapperProxyFactory.newInstance()
最終通過代理模式返回代理對(duì)象:
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]
{ mapperInterface }, mapperProxy);
回答了前面的問題:為什么要保存一個(gè)工廠類静稻,它是用來創(chuàng)建代理對(duì)象的警没。
JDK 動(dòng)態(tài)代理和 MyBatis 用到的 JDK 動(dòng)態(tài)代理有什么區(qū)別货岭?
JDK 動(dòng)態(tài)代理:
JDK 動(dòng)態(tài)代理代理扣汪,在實(shí)現(xiàn)了 InvocationHandler 的代理類里面,需要傳入一個(gè)被 代理對(duì)象的實(shí)現(xiàn)類校镐。
MyBatis 的動(dòng)態(tài)代理:
不需要實(shí)現(xiàn)類的原因:我們只需要根據(jù)接口類型+方法的名稱押搪,就可以找到 Statement ID 了树酪,而唯一要做的一件事情也是這件浅碾,所以不需要實(shí)現(xiàn)類。在 MapperProxy 里面直接執(zhí)行邏輯(也就是執(zhí)行 SQL)就可以续语。
總結(jié):
獲得 Mapper 對(duì)象的過程垂谢,實(shí)質(zhì)上是獲取了一個(gè) MapperProxy 的代理對(duì)象。 MapperProxy 中有 sqlSession疮茄、mapperInterface滥朱、methodCache。
先記下這個(gè)問題:在代理類中為什么要持有一個(gè) SqlSession娃豹?
4.執(zhí)行 SQL
Blog blog = mapper.selectBlog(1);
由于所有的 Mapper 都是 MapperProxy 代理對(duì)象焚虱,所以任意的方法都是執(zhí)行 MapperProxy 的 invoke()方法。
問題 1:我們引入 MapperProxy 為了解決什么問題懂版?硬編碼和編譯時(shí)檢查問題鹃栽。它需要做的事情是:根據(jù)方法查找 Statement ID 的問題。
問題 2:這里沒有實(shí)現(xiàn)類躯畴,進(jìn)入到 invoke 方法的時(shí)候做了什么事情民鼓?它是怎么找到 我們要執(zhí)行的 SQL 的?
我們看一下 invoke()方法:
-
MapperProxy.invoke()
-
首先判斷是否需要去執(zhí)行 SQL蓬抄,還是直接執(zhí)行方法丰嘉。
Object 本身的方法和 Java 8 中接口的默認(rèn)方法不需要去執(zhí)行 SQL。
思考:isDefaultMethod 判斷的是什么嚷缭?寫一個(gè)什么方法饮亏,它會(huì)走到這里? 這個(gè)是 Java 8 接口中默認(rèn)方法的示例:
public interface IService { default String getName(){ return "javacoo"; } }
-
獲取緩存
這里加入緩存是為了提升 MapperMethod 的獲取速度:
// 獲取緩存阅爽,保存了方法簽名和接口方法的關(guān)系 final MapperMethod mapperMethod = cachedMapperMethod(method);
Map 的 computeIfAbsent()方法:只有 key 不存在或者 value 為 null 的時(shí)候才調(diào)用 mappingFunction()路幸。
-
-
MapperMethod.execute()
接下來又調(diào)用了 mapperMethod 的 execute 方法:
mapperMethod.execute(sqlSession, args)
MapperMethod 里 面 主 要 有 兩 個(gè) 屬 性 , 一 個(gè) 是 SqlCommand 付翁, 一 個(gè) 是 MethodSignature简肴,這兩個(gè)都是 MapperMethod 的內(nèi)部類。
另外定義了多個(gè) execute()方法百侧。
在這一步砰识,根據(jù)不同的 type 和返回類型:
調(diào)用 convertArgsToSqlCommandParam()將參數(shù)轉(zhuǎn)換為 SQL 的參數(shù)。
調(diào)用 sqlSession 的 insert()佣渴、update()辫狼、delete()、selectOne ()方法观话,我們以查詢?yōu)槔杞瑁瑫?huì)走到 selectOne()方法。
-
DefaultSqlSession.selectOne()
selectOne()最終也是調(diào)用了 selectList()。
在 SelectList()中灵迫,我們先根據(jù) command name(Statement ID)從 Configuration 中拿到 MappedStatement秦叛,這個(gè) ms 上面有我們?cè)?xml 中配置的所有屬性,包括 id瀑粥、 statementType挣跋、sqlSource、useCache狞换、入?yún)⒈芘亍⒊鰠⒌鹊取?/p>
然后執(zhí)行了 Executor 的 query()方法。
前面我們說到了 Executor 有三種基本類型修噪,同學(xué)們還記得是哪幾種么查库?
SIMPLE/REUSE/BATCH,還有一種包裝類型黄琼,CachingExecutor樊销。 那么在這里到底會(huì)選擇哪一種執(zhí)行器呢? 我們要回過頭去看看 DefaultSqlSession 在初始化的時(shí)候是怎么賦值的脏款,這個(gè)就是 我們的會(huì)話創(chuàng)建過程围苫。
如果啟用了二級(jí)緩存,就會(huì)先調(diào)用 CachingExecutor 的 query()方法撤师,里面有緩存 相關(guān)的操作剂府,然后才是再調(diào)用基本類型的執(zhí)行器,比如默認(rèn)的 SimpleExecutor剃盾。
在沒有開啟二級(jí)緩存的情況下腺占,先會(huì)走到 BaseExecutor 的 query()方法(否則會(huì)先 走到 CachingExecutor)。
-
BaseExecutor.query()
-
創(chuàng)建 CacheKey
從 Configuration 中獲取 MappedStatement痒谴, 然后從 BoundSql 中獲取 SQL 信 息湾笛,創(chuàng)建 CacheKey。這個(gè) CacheKey 就是緩存的 Key闰歪。
然后再調(diào)用另一個(gè) query()方法。
-
清空本地緩存
queryStack 用于記錄查詢棧蓖墅,防止遞歸查詢重復(fù)處理緩存库倘。
flushCache=true 的時(shí)候,會(huì)先清理本地緩存(一級(jí)緩存):clearLocalCache();
如果沒有緩存论矾,會(huì)從數(shù)據(jù)庫查詢:queryFromDatabase()
如果 LocalCacheScope == STATEMENT教翩,會(huì)清理本地緩存。
-
從數(shù)據(jù)庫查詢
緩存:先在緩存用占位符占位贪壳。執(zhí)行查詢后饱亿,移除占位符,放入數(shù)據(jù)。
查詢:執(zhí)行 Executor 的 doQuery()彪笼;默認(rèn)是 SimpleExecutor钻注。
-
-
SimpleExecutor.doQuery()
-
創(chuàng)建 StatementHandler
在 configuration.newStatementHandler()中,new 一個(gè) StatementHandler配猫,先得到 RoutingStatementHandler幅恋。
RoutingStatementHandler 里 面 沒 有 任 何 的 實(shí) 現(xiàn) , 是 用 來 創(chuàng) 建 基 本 的 StatementHandler 的泵肄。這里會(huì)根據(jù) MappedStatement 里面的 statementType 決定 StatementHandler 的 類 型 捆交。 默 認(rèn) 是 PREPARED ( STATEMENT 、 PREPARED 腐巢、 CALLABLE)品追。
switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler,boundSql); break; case PREPARED: 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 里面包含了處理參數(shù)的 ParameterHandler 和處理結(jié)果集的 ResultSetHandler。
這兩個(gè)對(duì)象都是在上面 new 的時(shí)候創(chuàng)建的.
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql)
這三個(gè)對(duì)象都是可以被插件攔截的四大對(duì)象之一冯丙,所以在創(chuàng)建之后都要用攔截器進(jìn) 行包裝的方法肉瓦。
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
PS:四大對(duì)象還有一個(gè)是誰?在什么時(shí)候創(chuàng)建的银还?(Executor)
-
創(chuàng)建 Statement
用 new 出來的 StatementHandler 創(chuàng)建 Statement 對(duì)象——prepareStatement() 方法對(duì)語句進(jìn)行預(yù)編譯风宁,處理參數(shù)。
handler.parameterize(stmt) ;
-
執(zhí)行的 StatementHandler 的 query()方法
RoutingStatementHandler 的 query()方法蛹疯。
delegate 委派戒财,最終執(zhí)行 PreparedStatementHandler 的 query()方法。
-
執(zhí)行 PreparedStatement 的 execute()方法
后面就是 JDBC 包中的 PreparedStatement 的執(zhí)行了捺弦。
-
ResultSetHandler 處理結(jié)果集
return resultSetHandler.handleResultSets(ps);
問題:怎么把 ResultSet 轉(zhuǎn)換成 List<Object>饮寞?
ResultSetHandler 只有一個(gè)實(shí)現(xiàn)類:DefaultResultSetHandler。也就是執(zhí)行 DefaultResultSetHandler 的 handleResultSets ()方法列吼。
首先我們會(huì)先拿到第一個(gè)結(jié)果集幽崩,如果沒有配置一個(gè)查詢返回多個(gè)結(jié)果集的情況, 一般只有一個(gè)結(jié)果集寞钥。如果下面的這個(gè) while 循環(huán)我們也不用慌申,就是執(zhí)行一次。
然后會(huì)調(diào)用 handleResultSet()方法理郑。
【作業(yè)】總結(jié)一下蹄溉,MyBatis 里面用到了哪些設(shè)計(jì)模式?
-
一些信息
路漫漫其修遠(yuǎn)兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
郵箱:xihuady@126.com