目標(biāo)
1、 掌握 MyBatis 的工作流程
2、 掌握 MyBatis 的架構(gòu)分層與模塊劃分
3、 掌握 MyBatis 緩存機(jī)制
4、 通過閱讀 MyBatis 源碼掌握 MyBatis 底層工作原理與設(shè)計(jì)思想
首先在 MyBatis 啟動的時候我們要去解析配置文件,包括全局配置文件和映射器配置文件,這里面包含了我們怎么控制 MyBatis 的行為,和我們要對數(shù)據(jù)庫下達(dá)的指令,也就是我們的 SQL 信息。我們會把它們解析成一個 Configuration 對象。
接下來就是我們操作數(shù)據(jù)庫的接口,它在應(yīng)用程序和數(shù)據(jù)庫中間,代表我們跟數(shù)據(jù)庫之間的一次連接:這個就是 SqlSession 對象。
我 們 要 獲 得 一 個 會 話 , 必須有一 個會話工廠SqlSessionFactory 。SqlSessionFactory 里面又必須包含我們的所有的配置信息,所以我們會通過一個Builder 來創(chuàng)建工廠類。
我們知道,MyBatis 是對 JDBC 的封裝,也就是意味著底層一定會出現(xiàn) JDBC 的一些核心對象,比如執(zhí)行 SQL 的 Statement,結(jié)果集 ResultSet。在 Mybatis 里面,SqlSession 只是提供給應(yīng)用的一個接口,還不是 SQL 的真正的執(zhí)行對象。SqlSession 持有了一個 Executor 對象,用來封裝對數(shù)據(jù)庫的操作。
在執(zhí)行器 Executor 執(zhí)行 query 或者 update 操作的時候我們創(chuàng)建一系列的對象,來處理參數(shù)、執(zhí)行 SQL、處理結(jié)果集,這里我們把它簡化成一個對象:StatementHandler,在閱讀源碼的時候我們再去了解還有什么其他的對象。
這個就是 MyBatis 主要的工作流程,如圖:
MyBatis 架構(gòu)分層與模塊劃分
在 MyBatis 的主要工作流程里面,不同的功能是由很多不同的類協(xié)作完成的,它們分布在 MyBatis jar 包的不同的 package 里面。
我們來看一下 MyBatis 的 jar 包(基于 3.5.4)
大概有 300 多個類,這樣看起來不夠清楚,不知道什么類在什么環(huán)節(jié)工作,屬于什么層次。
跟 Spring 一樣,MyBatis 按照功能職責(zé)的不同,所有的 package 可以分成不同的工作層次。
我們可以把 MyBatis 的工作流程類比成餐廳的服務(wù)流程。
第一個是跟客戶打交道的服務(wù)員,它是用來接收程序的工作指令的,我們把它叫做接口層。
第二個是后臺的廚師,他們根據(jù)客戶的點(diǎn)菜單,把原材料加工成成品,然后傳到窗口。這一層是真正去操作數(shù)據(jù)的,我們把它叫做核心層。
最后就是餐廳也需要有人做后勤(比如清潔、采購、財務(wù)),來支持廚師的工作和整個餐廳的運(yùn)營。我們把它叫做基礎(chǔ)層。
來看一下這張圖,我們根據(jù)剛才的分層,和大體的執(zhí)行流程,做了這么一個總結(jié)。當(dāng)然,從不同的角度來描述,架構(gòu)圖的劃分有所區(qū)別,這張圖畫起來也有很多形式。我們先從總體上建立一個印象。每一層的主要對象和主要的功能我們也給大家分析一下。
接口層
首先接口層是我們打交道最多的。核心對象是 SqlSession,它是上層應(yīng)用和 MyBatis打交道的橋梁,SqlSession 上定義了非常多的對數(shù)據(jù)庫的操作方法。接口層在接收到調(diào)用請求的時候,會調(diào)用核心處理層的相應(yīng)模塊來完成具體的數(shù)據(jù)庫操作。
核心處理層
接下來是核心處理層。既然叫核心處理層,也就是跟數(shù)據(jù)庫操作相關(guān)的動作都是在這一層完成的。核心處理層主要做了這幾件事:
- 把接口中傳入的參數(shù)解析并且映射成 JDBC 類型;
- 解析 xml 文件中的 SQL 語句,包括插入?yún)?shù),和動態(tài) SQL 的生成;
- 執(zhí)行 SQL 語句;
- 處理結(jié)果集,并映射成 Java 對象。
插件也屬于核心層,這是由它的工作方式和攔截的對象決定的。
基礎(chǔ)支持層
最后一個就是基礎(chǔ)支持層。基礎(chǔ)支持層主要是一些抽取出來的通用的功能(實(shí)現(xiàn)復(fù)用),用來支持核心處理層的功能。比如數(shù)據(jù)源、緩存、日志、xml 解析、反射、IO、事務(wù)等等這些功能。
好了,大體架構(gòu)講解清楚了,讓我們正式開始閱讀源碼吧
MyBatis 源碼解讀
分析源碼,我們還是從編程式的 demo 入手,先貼上我的mybatis總體配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 打印查詢語句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局緩存(二級緩存)-->
<setting name="cacheEnabled" value="true"/>
<!-- 延遲加載的全局開關(guān)。當(dāng)開啟時,所有關(guān)聯(lián)對象都會延遲加載。默認(rèn) false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 當(dāng)開啟時,任何方法的調(diào)用都會加載該對象的所有屬性。默認(rèn) false,可通過select標(biāo)簽的 fetchType來覆蓋-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 創(chuàng)建具有延遲加載能力的對象所用到的代理工具,默認(rèn)JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT級別的緩存,使一級緩存,只針對當(dāng)前執(zhí)行的這一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.caecc.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.caecc.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 對象工廠 -->
<!-- <objectFactory type="com.caecc.objectfactory.GPObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.caecc.interceptor.SQLInterceptor">
<property name="gupao" value="betterme" />
</plugin>
<plugin interceptor="com.caecc.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 單獨(dú)使用時配置成MANAGED沒有事務(wù) -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
編寫測試類:
@Test
public void testSelect() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
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();
}
}
把文件讀取成流的這一步我們就省略了。所以下面我們分成四步來分析
第一步,我們通過建造者模式創(chuàng)建一個工廠類,配置文件的解析就是在這一步完成的,包括 mybatis-config.xml 和 Mapper 適配器文件。
問題:解析的時候怎么解析的,做了什么,產(chǎn)生了什么對象,結(jié)果存放到了哪里。解析的結(jié)果決定著我們后面有什么對象可以使用,和到哪里去取。
第二步,通過 SqlSessionFactory 創(chuàng)建一個 SqlSession。
問題:SqlSession 是用來操作數(shù)據(jù)庫的,返回了什么實(shí)現(xiàn)類,除了 SqlSession,還創(chuàng)建了什么對象,創(chuàng)建了什么環(huán)境?
第三步,獲得一個 Mapper 對象。
問題:Mapper 是一個接口,沒有實(shí)現(xiàn)類,是不能被實(shí)例化的,那獲取到的這個Mapper 對象是什么對象?為什么要從 SqlSession 里面去獲取?為什么傳進(jìn)去一個接口,然后還要用接口類型來接收?
第四步,調(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)換成對象的?接下來我們就會詳細(xì)分析每一步的流程,包括里面有哪些核心的對象和關(guān)鍵的方法
一、 配置解析過程
首先我們要清楚的是配置解析的過程全部只解析了兩種文件。一個是mybatis-config.xml 全局配置文件。另外就是可能有很多個的 Mapper.xml 文件,也包括在 Mapper 接口類上面定義的注解。
我們從 mybatis-config.xml 開始。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSessionFactoryBuilder這個類特別簡單,處理構(gòu)造函數(shù)就是build函數(shù),看著挺多,實(shí)際都是重載的函數(shù)。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 用于解析 mybatis-config.xml,同時創(chuàng)建了 Configuration 對象 >>
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析XML,最終返回一個 DefaultSqlSessionFactory >>
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
非常明顯的建造者模式,它里面定義了很多個 build 方法的重載,最終返回的是一個SqlSessionFactory 對象(單例模式)。我們點(diǎn)進(jìn)去 build 方法。build核心的重載函數(shù):
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 用于解析 mybatis-config.xml,同時創(chuàng)建了 Configuration 對象 >>
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析XML,最終返回一個 DefaultSqlSessionFactory >>
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
這里面創(chuàng)建了一個 XMLConfigBuilder 對象(Configuration 對象也是這個時候創(chuàng)建的)。
XMLConfigBuilder
XMLConfigBuilder 是抽象類 BaseBuilder 的一個子類,專門用來解析全局配置文件,針對不同的構(gòu)建目標(biāo)還有其他的一些子類,比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增刪改查標(biāo)簽
根據(jù)我們解析的文件流,這里后面兩個參數(shù)都是空的,創(chuàng)建了一個 parser。這里有兩步,第一步是調(diào)用 parser 的 parse()方法,它會返回一個 Configuration類。其實(shí)BaseBuilder作為抽象類,它這個名字起的不好,如果是spring代碼,肯定起AbstractBaseBuilder這個名字(懂的都懂)。插一嘴,程序員比的是代碼量嗎?不,是設(shè)計(jì)思想。
配置文件里面所有的信息都會放在Configuration 里面。其實(shí)Concifuration就放在基類BaseBuilder中:
番外:源碼看多了的讀者此時就能猜到,里面不是8個基本變量,就是各種集合。我工作寫框架就這么干,看名字覺著牛逼,到最底層就是各種變量和集合的維護(hù)而已。
Configuration 類里面有很多的屬性(自己去看,我不可能都貼不出來,想得美),有很多是跟 config 里面的標(biāo)簽直接對應(yīng)的,一目了然。其實(shí)mybatis的源碼特別直觀易讀,比Spring好讀多了,沒那么繞,Spring讀完一遍,人瘋了。那么Configuration啥時候創(chuàng)建的呢,就在XMLConfigBuilder的構(gòu)造器里:
簡單看一下Configuration的構(gòu)造方法:
什么? 原來就是別名注冊,啥叫注冊?大部分源碼框架里的注冊就是往HashMap里面堆數(shù)據(jù)!不信你看:
這時候看一下的我配置文件的這個配置:
我們先看一下 parse()方法:
首先會檢查是不是已經(jīng)解析過,也就是說在應(yīng)用的生命周期里面,config 配置文件只需要解析一次,生成的 Configuration 對象也會存在應(yīng)用的整個生命周期中。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// XPathParser,dom 和 SAX 都有用到 >>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
接下來就是 parseConfiguration 方法:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 對于全局配置文件各種標(biāo)簽的解析
propertiesElement(root.evalNode("properties"));
// 解析 settings 標(biāo)簽
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 類型別名
typeAliasesElement(root.evalNode("typeAliases"));
// 插件
pluginElement(root.evalNode("plugins"));
// 用于創(chuàng)建對象
objectFactoryElement(root.evalNode("objectFactory"));
// 用于對對象進(jìn)行加工
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工具箱
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 子標(biāo)簽賦值,默認(rèn)值就是在這里提供的 >>
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 創(chuàng)建了數(shù)據(jù)源 >>
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析引用的Mapper映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
全是解析的工作。這下面有十幾個方法,對應(yīng)著 config 文件里面的所有一級標(biāo)簽。接下來我們就挨個分析分析,我就喜歡一行一行扣源碼,真有意思。
propertiesElement(root.evalNode("properties"));
第一個是解析<properties>標(biāo)簽,讀取我們引入的外部配置文件。這里面又有兩種類型,一種是放在 resource 目錄下的,是相對路徑,一種是寫的絕對路徑的。
看我的mybatis.config中有這么一個配置
看源碼,我很用心寫了注釋,相信你看的懂:
解析的最終結(jié)果就是我們會把所有的配置信息放到名為 defaults 的 Properties 對象里面,最后把XPathParser 和 Configuration 的 Properties 屬性都設(shè)置成我們填充后的 Properties對象。其實(shí)指向的都是同一個對象。
settingsAsProperties()
第二個,我們把<settings>標(biāo)簽也解析成了一個 Properties 對象,對于<settings>標(biāo)簽的子標(biāo)簽的處理在后面。
對應(yīng)mybatis-config.xml配置文件中:
loadCustomVfs(settings)
對這個方法不感興趣,我用不到!不讀了。
loadCustomLogImpl(settings)
loadCustomLogImpl 是根據(jù)<logImpl>標(biāo)簽獲取日志的實(shí)現(xiàn)類,我們可以用到很多的日志的方案,包括 LOG4J,LOG4J2,SLF4J 等等。這里生成了一個 Log 接口的實(shí)現(xiàn)類,并且賦值到 Configuration 中。
源碼很簡單,不貼了,反正沒人看,我就是做筆記而已。
typeAliasesElement()
接下來,我們解析<typeAliases>標(biāo)簽,我們在講配置的時候也講過,它有兩種定義方式,一種是直接定義一個類的別名,一種就是指定一個包,那么這個 package 下面所有的類的名字就會成為這個類全路徑的別名。類的別名和類的關(guān)系,我們放在一個 TypeAliasRegistry 對象里面。
pluginElement()
接下來就是解析<plugins>標(biāo)簽,比如 Pagehelper 的翻頁插件,或者我們自定義的插件。<plugins>標(biāo)簽里面只有<plugin>標(biāo)簽,<plugin>標(biāo)簽里面只有<property>標(biāo)簽。
標(biāo)簽解析完以后,會生成一個 Interceptor 對象,并且添加到 Configuration 的InterceptorChain 屬性里面,它是一個 List。這個后面再說。
objectFactoryElement()、 objectWrapperFactoryElement()
接下來的兩個標(biāo)簽是用來實(shí)例化對象用的 ,<objectFactory>和<objectWrapperFactory> 這兩個標(biāo) 簽 ,分別生成ObjectFactory 、ObjectWrapperFactory對象,通過反射生成對象設(shè)置到 Configuration 的屬性里面。
reflectorFactoryElement()
解析 reflectorFactory 標(biāo)簽,生成 ReflectorFactory 對象(在官方 的 pdf 文檔里面沒有找到這個配置)。反射工具箱。
settingsElement(settings)
這里就是對<settings>標(biāo)簽里面所有子標(biāo)簽的處理了,前面我們已經(jīng)把子標(biāo)簽全部轉(zhuǎn)換成了 Properties 對象,所以在這里處理 Properties 對象就可以了。
二級標(biāo)簽里面有很多的配置,比如二級緩存,延遲加載,自動生成主鍵這些。需要注意的是,我們之前提到的所有的默認(rèn)值,都是在這里賦值的。如果說后面我們不知道這個屬性的值是什么,也可以到這一步來確認(rèn)一下。
所有的值,都會賦值到 Configuration 的屬性里面去。
這些屬性都設(shè)置在Configuration對象里,所以,當(dāng)你不知道setting下有哪些屬性可以設(shè)置時,不妨看看這里,我這里從別的文章中截圖簡要說明一下上面屬性的意思,到目前為止只是解析配置,至于怎么用這些配置,別著急,后面會說。
大體有個印象,回查。
environmentsElement()
這一步是解析<environments>標(biāo)簽。一個 environment 就是對應(yīng)一個數(shù)據(jù)源,所以在這里我們會根據(jù)配置的<transactionManager>創(chuàng)建一個事務(wù)工廠,根據(jù)<dataSource>標(biāo)簽創(chuàng)建一個數(shù)據(jù)源,最后把這兩個對象設(shè)置成 Environment 對象的屬性,放到 Configuration 里面。關(guān)于數(shù)據(jù)庫連接池,讀者(我自己)可以參考這篇文章https://blog.csdn.net/crave_shy/article/details/46611205。
databaseIdProviderElement()
解析 databaseIdProvider 標(biāo)簽,生成 DatabaseIdProvider 對象(用來支持不同廠商的數(shù)據(jù)庫)。
typeHandlerElement()
跟 TypeAlias 一樣,TypeHandler 有兩種配置方式,一種是單獨(dú)配置一個類,一種是指定一個 package。最后我們得到的是JavaType和JdbcType,以及用來做相互映射的TypeHandler 之間的映射關(guān)系。最后存放在 TypeHandlerRegistry 對象里面。
mapperElement(root.evalNode("mappers"));
我們現(xiàn)在就要來定義 SQL 映射語句了。 但首先,我們需要告訴 MyBatis 到哪里去找到這些語句。 在自動查找資源方面,Java 并沒有提供一個很好的解決方案,所以最好的辦法是直接告訴 MyBatis 到哪里去找映射文件。 你可以使用相對于類路徑的資源引用,或完全限定資源定位符(包括 file:/// 形式的 URL),或類名和包名等。例如:
<!-- 使用相對于類路徑的資源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定資源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口實(shí)現(xiàn)類的完全限定類名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 將包內(nèi)的映射器接口實(shí)現(xiàn)全部注冊為映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
這些配置會告訴 MyBatis 去哪里找映射文件,剩下的細(xì)節(jié)就應(yīng)該是每個 SQL 映射文件了,這個沒啥還糾結(jié)的,定死的,代碼無非就是按這幾種方式解析,if..else..罷了。無論是按 package 掃描,還是按接口掃描,最后都會調(diào)用到 MapperRegistry 的addMapper()方法。MapperRegistry 里面維護(hù)的其實(shí)是一個 Map 容器,存儲接口和代理工廠的映射關(guān)系。詳細(xì)看一下如何解析的Mapper吧:
// 通過解析 mappers 節(jié)點(diǎn),找到Mapper文件
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 查看mappers節(jié)點(diǎn)中是否有 package 節(jié)點(diǎn),有就解析,否則就解析 mapper子節(jié)點(diǎn)
/*
<mappers>
<package name="com.test"/>
<!-- <mapper resource="mapper/DemoMapper.xml"/> -->
</mappers>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 解析 mapper 節(jié)點(diǎn),mapper 節(jié)點(diǎn)中有三個屬性(resource、url、class),但是只能存在一個
/**
* resource:表示文件夾下xml文件
* <mapper resource="mapper/DemoMapper.xml"/>
*
* class:DemoMapper 動態(tài)代理接口,DemoMapper.xml文件
* <mapper class="com.test.DemoMapper" />
*
* url:表示盤符下的絕對路徑,絕對不推薦使用
* <mapper resource="F:\javaEE\workspace\MyBatisDemo_My\mybatis-3-master\src\main\resources\mapper\DemoMapper.xml"/>
*
*/
/*
<mappers>
<!-- <package name="com.test"/> -->
<mapper resource="mapper/DemoMapper.xml"/>
</mappers>
*/
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 三個屬性只能是其中的一個有value,否則就拋異常
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 如果resource有值,就解析mapper.xml文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 如果url有值,則通過絕對路徑獲取輸入流,獲取mapper.xml并解析
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對象
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.");
}
}
}
}
}
首先是包掃描,啥意思?比如我的mybaits-config.xml文件是這么配置的(實(shí)際在springboot項(xiàng)目中就是則會么配置的):
意思就是掃描該包下所有的mapper文件。
if ("package".equals(child.getName())) {
//獲取包的名稱
String mapperPackage = child.getStringAttribute("name");
//解析包下的mapper文件
configuration.addMappers(mapperPackage);
}
進(jìn)入到addMappers方法里面詳細(xì)看一下“
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public void addMappers(String packageName) {
//
mapperRegistry.addMappers(packageName);
}
還是configuration中的mapper注冊工廠mapperRegistry,里面就是封裝了HashMap
進(jìn)入到addMappers方法
就是遍歷這個包下的所有class,然后調(diào)用addMapper方法,進(jìn)入看看
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {//判斷是不是接口,Mapper文件是一個接口,后面會動態(tài)代理生成一個實(shí)體
if (hasMapper(type)) {//判斷改接口是否已經(jīng)注冊過了
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// !Map<Class<?>, MapperProxyFactory<?>> 存放的是接口類型,和對應(yīng)的工廠類的關(guān)系,工廠就是存放了接口的類型及相關(guān)方法,將來反射用
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// 注冊了接口之后,根據(jù)接口,開始解析所有方法上的注解,例如 @Select >>
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
拿到mapper,后就看是解析這個mapper,最主要的就是這個MapperAnnotationBuilder的parse方法,根據(jù)名字一看就是注解解析(其實(shí)我們也很少在Mapper里以注解的方式寫sql),看看吧:
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();
}
不知道你曾經(jīng)好奇過沒有,Mapper接口是怎么跟xml文件對應(yīng)起來的,就是在這里,loadXmlResource這個方法:
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//拿到類名,然后根據(jù)類名在后面加上.xml
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 解析 Mapper.xml >>
xmlParser.parse();
}
}
}
他會根據(jù)你的類全路徑名,在后面改成.xml,然后在相同的目錄下找到這個xml文件,所以你的xml文件資源全路徑必須跟類全路徑一致。
xmlParser.parse();才是真正的解析xml文件:
public void parse() {
// 總體上做了兩件事情,對于語句的注冊和接口的注冊
if (!configuration.isResourceLoaded(resource)) {
// 1、具體增刪改查標(biāo)簽的解析。
// 一個標(biāo)簽一個MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(接口類型)和工廠類綁定起來,放到一個map。
// 一個namespace 一個 MapperProxyFactory >>
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
parser.evalNode("/mapper")相當(dāng)于拿到了整個mapper.xml的文件內(nèi)容,然后依次解析各個標(biāo)簽,后面?zhèn)溆谩onfigurationElement方法進(jìn)入:
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"));
// 創(chuàng)建 ParameterMapping 對象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 創(chuàng)建 List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可以復(fù)用的SQL
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增刪改查標(biāo)簽,得到 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);
}
}
方法很多,我就不一一解析了,反正就是解析,然后存內(nèi)存里,后面用的話會慢慢從內(nèi)存中拿到配置就行匹配。
如果你不是根據(jù)package方法配置的,就走下面的resource和url,其實(shí)都是一樣的,無非就是少了一層Mapper接口到xml的映射。不想寫了,自己看。
小結(jié)
在這一步,我們主要完成了 config 配置文件、Mapper 文件、Mapper 接口上的注解的解析。
我們得到了一個最重要的對象 Configuration,這里面存放了全部的配置信息,它在屬性里面還有各種各樣的容器后,返回了一個 DefaultSqlSessionFactory,里面持有了 Configuration 的實(shí)例。
二、會話創(chuàng)建過程
解析工作結(jié)束了,所有東西都在Configuration,都是靜態(tài)的,悄悄的躺在內(nèi)存里,現(xiàn)在我們讓他們動起來。
下一步訪問數(shù)據(jù)庫,訪問之前我們需要先跟數(shù)據(jù)庫建立一條鏈接,也就是創(chuàng)建一條會話
SqlSession session = sqlSessionFactory.openSession();
進(jìn)入DefaultSqlSessionFactory的openSession方法,一直跟下去:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 獲取事務(wù)工廠
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 創(chuàng)建事務(wù)
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根據(jù)事務(wù)工廠和默認(rèn)的執(zhí)行器類型,創(chuàng)建執(zhí)行器 >>
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
首先向我們走來的是enviroment代表隊(duì),就是他啦
然后我們根據(jù)這個配置呢,就開始創(chuàng)建事務(wù)工廠,這里從Environment對象中取出一個TransactionFactory,它是解析evironments標(biāo)簽的時候創(chuàng)建的。autoCommit默認(rèn)為false。我們的事務(wù)管理器是JDBC類型,所以我們返回來的事務(wù)也是JDBC類型:
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
他會使用connection對象的commit()、rollbach()、close()管理事務(wù),這個connection就是驅(qū)動提供的鏈接
源碼也很簡單
如果配置成MANAGED,會把事務(wù)交給容器來管理,比如JBOSS,Weblogic。因?yàn)槲覀兣艿氖潜镜爻绦蚺渲贸蒑ANAGED不會有任何事務(wù)。如果是spring+mybati,則沒有必要配置,因?yàn)槲覀儠赼pplicationContxt.xml里面配置數(shù)據(jù)源和事務(wù)管理器,覆蓋mybatis的配置。
下一步就是創(chuàng)建執(zhí)行器final Executor executor = configuration.newExecutor(tx, execType);
,可以細(xì)分成3個步驟:
- 創(chuàng)建執(zhí)行器
Executor的基本類型有3種:
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
默認(rèn)就是SIMPLE
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默認(rèn) SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
一看到這個Base開頭的抽象類,就知道他肯定存儲著公共變量和公共方法,提高了代碼的復(fù)用性,在策略模式和模板方法模式中很常用。在這里就用到了模板方法模式
模板方法模式,定義了一個算法骨架,并允許子類為一個或多個步驟提供實(shí)現(xiàn)。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法的某些步驟。
- 緩存裝飾
如果cacheEnable=true,會用裝飾器模式對executor進(jìn)行裝飾。
// 二級緩存開關(guān),settings 中的 cacheEnabled 默認(rèn)是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
- 插件代理
// 植入插件的邏輯,至此,四大對象已經(jīng)全部攔截完畢
executor = (Executor) interceptorChain.pluginAll(executor);
- 返回SqlSession的實(shí)現(xiàn)
最終返回DefaultSession,它的屬性包括Configuration、Eexecutor對象。
return new DefaultSqlSession(configuration, executor, autoCommit);
小結(jié)
創(chuàng)建會話的過程,我們獲得了一個DefaultSqlSession,里面包含了一個Executor,Executor是SQL的實(shí)際執(zhí)行對象。
三、獲得Mapper對象
現(xiàn)在我們已經(jīng)有一個 DefaultSqlSession 了,必須找到 Mapper.xml 里面定義的Statement ID,才能執(zhí)行對應(yīng)的 SQL 語句。找到 Statement ID 有兩種方式:一種是直接調(diào)用 session 的方法,在參數(shù)里面?zhèn)魅隨tatement ID,這種方式屬于硬編碼,我們沒辦法知道有多少處調(diào)用,修改起來也很麻煩。
在 MyBatis 后期的版本提供了第二種方式,就是定義一個接口,然后再調(diào)用Mapper 接口的方法。由于我們的接口名稱跟 Mapper.xml 的 namespace 是對應(yīng)的,接口的方法跟statement ID 也都是對應(yīng)的,所以根據(jù)方法就能找到對應(yīng)的要執(zhí)行的 SQL。
BlogMapper mapper = session.getMapper(BlogMapper.class);
在這里我們主要研究一下 Mapper 對象是怎么獲得的,它的本質(zhì)是什么。
DefaultSqlSession 的 getMapper()方法,調(diào)用了 Configuration 的 getMapper()方法。
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Configuration 的 getMapper()方法,又調(diào)用了 MapperRegistry 的 getMapper()方法。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
我們知道,在解析 mapper 標(biāo)簽和 Mapper.xml 的時候已經(jīng)把接口類型和類型對應(yīng)的MapperProxyFactory 放到了一個 Map 中。獲取 Mapper 代理對象,實(shí)際上是從Map 中獲取對應(yīng)的工廠類后,調(diào)用以下方法創(chuàng)建對象:
mapperProxyFactory.newInstance(sqlSession);
最終通過代理模式返回代理對象:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 1:類加載器:2:被代理類實(shí)現(xiàn)的接口、3:實(shí)現(xiàn)了 InvocationHandler 的觸發(fā)管理類
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
獲得 Mapper 對象的過程,實(shí)質(zhì)上是獲取了一個 MapperProxy 的代理對象。MapperProxy 中有 sqlSession、mapperInterface、methodCache。
四、執(zhí)行SQL
Blog blog = mapper.selectBlog(1);
由于所有的 Mapper 都是 MapperProxy 代理對象,所以任意的方法都是執(zhí)行MapperProxy 的 invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// toString hashCode equals getClass等方法,無需走到執(zhí)行SQL的流程
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 提升獲取 mapperMethod 的效率,到 MapperMethodInvoker(內(nèi)部接口) 的 invoke
// 普通方法會走到 PlainMethodInvoker(內(nèi)部類) 的 invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
首先判斷是否需要去執(zhí)行 SQL,還是直接執(zhí)行方法。Object 本身的方法和 Java 8 中接口的默認(rèn)方法不需要去執(zhí)行 SQL。
cachedInvoker會對方法做一個緩存處理
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// Java8 中 Map 的方法,根據(jù) key 獲取值,如果值是 null,則把后面Object 的值賦給 key
// 如果獲取不到,就創(chuàng)建
// 獲取的是 MapperMethodInvoker(接口) 對象,只有一個invoke方法
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
// 接口的默認(rèn)方法(Java8),只要實(shí)現(xiàn)接口都會繼承接口的默認(rèn)方法,例如 List.sort()
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 創(chuàng)建了一個 MapperMethod
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
這里加入緩存是為了提升 MapperMethod 的獲取速度,Map 的 computeIfAbsent()方法:只有 key 不存在或者 value 為 null 的時候才調(diào)用 mappingFunction()。最后調(diào)用了下面方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// SQL執(zhí)行的真正起點(diǎn)
return mapperMethod.execute(sqlSession, args);
}
MapperMethod里面主要有兩個屬性,一個是SqlCommand,一個是MethodSignature,這兩個都是 MapperMethod 的內(nèi)部類。
另外,還定義了多個execute方法
在這一步,根據(jù)不同的 type 和返回類型:
調(diào)用 convertArgsToSqlCommandParam()將參數(shù)轉(zhuǎn)換為 SQL 的參數(shù)。
Object param = method.convertArgsToSqlCommandParam(args);
調(diào)用 sqlSession 的 insert()、update()、delete()、selectOne ()方法,我們以查詢
為例,會走到 selectOne()方法。
result = sqlSession.selectOne(command.getName(), param);
DefaultSqlSession.selectOne()
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;
}
}
selectOne()最終也是調(diào)用了 selectList()。
public <E> List<E> selectList(String statement, Object parameter) {
// 為了提供多種重載(簡化方法使用),和默認(rèn)值
// 讓參數(shù)少的調(diào)用參數(shù)多的方法,只實(shí)現(xiàn)一次
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
在 SelectList()中,我們先根據(jù) command name(Statement ID)從 Configuration中拿到 MappedStatement,這個 ms 上面有我們在 xml 中配置的所有屬性,包括 id、statementType、sqlSource、useCache、入?yún)ⅰ⒊鰠⒌鹊取?/p>
然后執(zhí)行了 Executor 的 query()方法。
前面我們說到了 Executor 有三種基本類型,同學(xué)們還記得是哪幾種么?
SIMPLE/REUSE/BATCH,還有一種包裝類型,CachingExecutor。
那么在這里到底會選擇哪一種執(zhí)行器呢?
我們要回過頭去看看 DefaultSqlSession 在初始化的時候是怎么賦值的,這個就是我們的會話創(chuàng)建過程。
如果啟用了二級緩存,就會先調(diào)用 CachingExecutor 的 query()方法,里面有緩存相關(guān)的操作,然后才是再調(diào)用基本類型的執(zhí)行器,比如默認(rèn)的 SimpleExecutor。
在沒有開啟二級緩存的情況下,先會走到 BaseExecutor 的 query()方法(否則會先走到 CachingExecutor)。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 獲取SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 創(chuàng)建CacheKey:什么樣的SQL是同一條SQL? >>
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
首先找到綁定的sql,然后從 Configuration 中獲取 MappedStatement, 然后從 BoundSql 中獲取 SQL 信息,創(chuàng)建 CacheKey。這個 CacheKey 就是緩存的 Key。
CachingExecutor 走完了會走BaseExecutor 的query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 異常體系之 ErrorContext
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// flushCache="true"時,即使是查詢,也清空一級緩存
clearLocalCache();
}
List<E> list;
try {
// 防止遞歸查詢重復(fù)處理緩存
queryStack++;
// 查詢一級緩存
// ResultHandler 和 ResultSetHandler的區(qū)別
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 真正的查詢流程
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
queryStack 用于記錄查詢棧,防止遞歸查詢重復(fù)處理緩存。
flushCache=true 的時候,會先清理本地緩存(一級緩存):clearLocalCache();
如果沒有緩存,會從數(shù)據(jù)庫查詢:queryFromDatabase()
如果 LocalCacheScope == STATEMENT,會清理本地緩存。
queryFromDatabase()執(zhí)行方法如下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 三種 Executor 的區(qū)別,看doUpdate
// 默認(rèn)Simple
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 寫入一級緩存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
先在緩存用占位符占位。執(zhí)行查詢后,移除占位符,放入數(shù)據(jù)。執(zhí)行 Executor 的 doQuery(),默認(rèn)是 SimpleExecutor。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 注意,已經(jīng)來到SQL處理的關(guān)鍵對象 StatementHandler >>
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 獲取一個 Statement對象
stmt = prepareStatement(handler, ms.getStatementLog());
// 執(zhí)行查詢
return handler.query(stmt, resultHandler);
} finally {
// 用完就關(guān)閉
closeStatement(stmt);
}
}
創(chuàng)建 StatementHandler
在 configuration.newStatementHandler()中,new 一個 StatementHandler,先得到 RoutingStatementHandler。
RoutingStatementHandler里面沒有任何的實(shí)現(xiàn) ,是用來創(chuàng)建基本的StatementHandler 的。這里會根據(jù) MappedStatement 里面的 statementType 決定StatementHandler 的 類 型 。 默 認(rèn) 是 PREPARED ( STATEMENT 、 PREPARED 、CALLABLE)。
StatementHandler 里面包含了處理參數(shù)的 ParameterHandler 和處理結(jié)果集的ResultSetHandler。創(chuàng)建 Statement
用 new 出來的 StatementHandler 創(chuàng)建 Statement 對象——prepareStatement()方法對語句進(jìn)行預(yù)編譯,處理參數(shù)。
handler.parameterize(stmt) ;
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 獲取 Statement 對象
stmt = handler.prepare(connection, transaction.getTimeout());
// 為 Statement 設(shè)置參數(shù)
handler.parameterize(stmt);
return stmt;
}
執(zhí)行的 StatementHandler 的 query()方法
RoutingStatementHandler 的 query()方法。
delegate 委派,最終執(zhí)行 PreparedStatementHandler 的 query()方法。執(zhí)行 PreparedStatement 的 execute()方法
后面就是 JDBC 包中的 PreparedStatement 的執(zhí)行了。ResultSetHandler 處理結(jié)果集
return resultSetHandler.handleResultSets(ps);
ResultSetHandler 只有一個實(shí)現(xiàn)類:DefaultResultSetHandler。也就是執(zhí)DefaultResultSetHandler 的 handleResultSets ()方法。
首先我們會先拿到第一個結(jié)果集,如果沒有配置一個查詢返回多個結(jié)果集的情況,一般只有一個結(jié)果集。如果下面的這個 while 循環(huán)我們也不用,就是執(zhí)行一次。然后會調(diào)用 handleResultSet()方法。