日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

MyBatis 源码分析 - 映射文件解析过程

發(fā)布時間:2025/3/21 编程问答 21 豆豆
生活随笔 收集整理的這篇文章主要介紹了 MyBatis 源码分析 - 映射文件解析过程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1.簡介

在上一篇文章中,我詳細(xì)分析了 MyBatis 配置文件的解析過程。由于上一篇文章的篇幅比較大,加之映射文件解析過程也比較復(fù)雜的原因。所以我將映射文件解析過程的分析內(nèi)容從上一篇文章中抽取出來,獨(dú)立成文,于是就有了本篇文章。在本篇文章中,我將分析映射文件中出現(xiàn)的一些及節(jié)點(diǎn),比如 <cache>,<cache-ref>,<resultMap>, <select | insert | update | delete> 等。除了分析常規(guī)的 XML 解析過程外,我還會向大家介紹 Mapper 接口的綁定過程等。綜上所述,本篇文章內(nèi)容會比較豐富,如果大家對此感興趣,不妨花點(diǎn)時間讀一讀,會有新的收獲。當(dāng)然,本篇文章通篇是關(guān)于源碼分析的,所以閱讀本文需要大家對 MyBatis 有一定的了解。如果大家對 MyBatis 還不是很了解,建議閱讀一下 MyBatis 的官方文檔。

其他的就不多說了,下面開始我們的 MyBatis 源碼之旅。

?2.映射文件解析過程分析

我在前面說過,映射文件的解析過程是 MyBatis 配置文件解析過程的一部分。MyBatis 的配置文件由 XMLConfigBuilder 的 parseConfiguration 進(jìn)行解析,該方法依次解析了 <properties>、<settings>、<typeAliases> 等節(jié)點(diǎn)。至于 <mappers> 節(jié)點(diǎn),parseConfiguration 則是在方法的結(jié)尾對其進(jìn)行了解析。該部分的解析邏輯封裝在 mapperElement 方法中,下面來看一下。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 // -☆- XMLConfigBuilder private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {// 獲取 <package> 節(jié)點(diǎn)中的 name 屬性String mapperPackage = child.getStringAttribute("name");// 從指定包中查找 mapper 接口,并根據(jù) mapper 接口解析映射配置configuration.addMappers(mapperPackage);} else {// 獲取 resource/url/class 等屬性String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapperClass = child.getStringAttribute("class");// resource 不為空,且其他兩者為空,則從指定路徑中加載配置if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());// 解析映射文件mapperParser.parse();// url 不為空,且其他兩者為空,則通過 url 加載配置} else if (resource == null && url != null && mapperClass == null) {ErrorContext.instance().resource(url);InputStream inputStream = Resources.getUrlAsStream(url);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());// 解析映射文件mapperParser.parse();// mapperClass 不為空,且其他兩者為空,則通過 mapperClass 解析映射配置} else if (resource == null && url == null && mapperClass != null) {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.");}}}} }

上面的代碼比較簡單,主要邏輯是遍歷 mappers 的子節(jié)點(diǎn),并根據(jù)節(jié)點(diǎn)屬性值判斷通過什么方式加載映射文件或映射信息。這里,我把配置在注解中的內(nèi)容稱為映射信息,以 XML 為載體的配置稱為映射文件。在 MyBatis 中,共有四種加載映射文件或信息的方式。第一種是從文件系統(tǒng)中加載映射文件;第二種是通過 URL 的方式加載和解析映射文件;第三種是通過 mapper 接口加載映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一種是通過包掃描的方式獲取到某個包下的所有類,并使用第三種方式為每個類解析映射信息。

以上簡單介紹了 MyBatis 加載映射文件或信息的幾種方式。需要注意的是,在 MyBatis 中,通過注解配置映射信息的方式是有一定局限性的,這一點(diǎn) MyBatis 官方文檔中描述的比較清楚。這里引用一下:

因?yàn)樽畛踉O(shè)計(jì)時,MyBatis 是一個 XML 驅(qū)動的框架。配置信息是基于 XML 的,而且映射語句也是定義在 XML 中的。而到了 MyBatis 3,就有新選擇了。MyBatis 3 構(gòu)建在全面且強(qiáng)大的基于 Java 語言的配置 API 之上。這個配置 API 是基于 XML 的 MyBatis 配置的基礎(chǔ),也是新的基于注解配置的基礎(chǔ)。注解提供了一種簡單的方式來實(shí)現(xiàn)簡單映射語句,而不會引入大量的開銷。

注意:?不幸的是,Java 注解的的表達(dá)力和靈活性十分有限。盡管很多時間都花在調(diào)查、設(shè)計(jì)和試驗(yàn)上,最強(qiáng)大的 MyBatis 映射并不能用注解來構(gòu)建——并不是在開玩笑,的確是這樣。

如上所示,重點(diǎn)語句我用黑體標(biāo)注了出來。限于 Java 注解的表達(dá)力和靈活性,通過注解的方式并不能完全發(fā)揮 MyBatis 的能力。所以,對于一些較為復(fù)雜的配置信息,我們還是應(yīng)該通過 XML 的方式進(jìn)行配置。正因此,在接下的章節(jié)中,我會重點(diǎn)分析基于 XML 的映射文件的解析過程。如果能弄懂此種配置方式的解析過程,那么基于注解的解析過程也不在話下。

下面開始分析映射文件的解析過程,在展開分析之前,先來看一下映射文件解析入口。如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // -☆- XMLMapperBuilder public void parse() {// 檢測映射文件是否已經(jīng)被解析過if (!configuration.isResourceLoaded(resource)) {// 解析 mapper 節(jié)點(diǎn)configurationElement(parser.evalNode("/mapper"));// 添加資源路徑到“已解析資源集合”中configuration.addLoadedResource(resource);// 通過命名空間綁定 Mapper 接口bindMapperForNamespace();}// 處理未完成解析的節(jié)點(diǎn)parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements(); }

如上,映射文件解析入口邏輯包含三個核心操作,分別如下:

  • 解析 mapper 節(jié)點(diǎn)
  • 通過命名空間綁定 Mapper 接口
  • 處理未完成解析的節(jié)點(diǎn)
  • 這三個操作對應(yīng)的邏輯,我將會在隨后的章節(jié)中依次進(jìn)行分析。下面,先來分析第一個操作對應(yīng)的邏輯。

    ?2.1 解析映射文件

    在 MyBatis 映射文件中,可以配置多種節(jié)點(diǎn)。比如 <cache>,<resultMap>,<sql> 以及 <select | insert | update | delete> 等。下面我們來看一個映射文件配置示例。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <mapper namespace="xyz.coolblog.dao.AuthorDao"><cache/><resultMap id="authorResult" type="Author"><id property="id" column="id"/><result property="name" column="name"/><!-- ... --></resultMap><sql id="table">author</sql><select id="findOne" resultMap="authorResult">SELECTid, name, age, sex, emailFROM<include refid="table"/>WHEREid = #{id}</select><!-- <insert|update|delete/> --> </mapper>

    上面是一個比較簡單的映射文件,還有一些的節(jié)點(diǎn)沒有出現(xiàn)在上面。以上每種配置中的每種節(jié)點(diǎn)的解析邏輯都封裝在了相應(yīng)的方法中,這些方法由 XMLMapperBuilder 類的 configurationElement 方法統(tǒng)一調(diào)用。該方法的邏輯如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private void configurationElement(XNode context) {try {// 獲取 mapper 命名空間String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}// 設(shè)置命名空間到 builderAssistant 中builderAssistant.setCurrentNamespace(namespace);// 解析 <cache-ref> 節(jié)點(diǎn)cacheRefElement(context.evalNode("cache-ref"));// 解析 <cache> 節(jié)點(diǎn)cacheElement(context.evalNode("cache"));// 已廢棄配置,這里不做分析parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析 <resultMap> 節(jié)點(diǎn)resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析 <sql> 節(jié)點(diǎn)sqlElement(context.evalNodes("/mapper/sql"));// 解析 <select>、...、<delete> 等節(jié)點(diǎn)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);} }

    上面代碼的執(zhí)行流程清晰明了。在閱讀源碼時,我們可以按部就班的分析每個方法調(diào)用即可。不過在寫文章進(jìn)行敘述時,需要做一些調(diào)整。下面我將會先分析 <cache> 節(jié)點(diǎn)的解析過程,然后再分析 <cache-ref> 節(jié)點(diǎn),之后會按照順序分析其他節(jié)點(diǎn)的解析過程。接下來,我們來看看 <cache> 節(jié)點(diǎn)的解析過程。

    ?2.1.1 解析 <cache> 節(jié)點(diǎn)

    MyBatis 提供了一、二級緩存,其中一級緩存是 SqlSession 級別的,默認(rèn)為開啟狀態(tài)。二級緩存配置在映射文件中,使用者需要顯示配置才能開啟。如果沒有特殊要求,二級緩存的配置很容易。如下:

    1 <cache/>

    如果我們想修改緩存的一些屬性,可以像下面這樣配置。

    1 2 3 4 5 <cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>

    根據(jù)上面的配置創(chuàng)建出的緩存有以下特點(diǎn):

  • 按先進(jìn)先出的策略淘汰緩存項(xiàng)
  • 緩存的容量為 512 個對象引用
  • 緩存每隔60秒刷新一次
  • 緩存返回的對象是寫安全的,即在外部修改對象不會影響到緩存內(nèi)部存儲對象
  • 除了上面兩種配置方式,我們還可以給 MyBatis 配置第三方緩存或者自己實(shí)現(xiàn)的緩存等。比如,我們將 Ehcache 緩存整合到 MyBatis 中,可以這樣配置。

    1 2 3 4 5 6 7 <cache type="org.mybatis.caches.ehcache.EhcacheCache"/><property name="timeToIdleSeconds" value="3600"/><property name="timeToLiveSeconds" value="3600"/><property name="maxEntriesLocalHeap" value="1000"/><property name="maxEntriesLocalDisk" value="10000000"/><property name="memoryStoreEvictionPolicy" value="LRU"/> </cache>

    以上簡單介紹了幾種緩存配置方式,關(guān)于 MyBatis 緩存更多的知識,后面我會獨(dú)立成文進(jìn)行分析,這里就不深入說明了。下面我們來分析一下緩存配置的解析邏輯,如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void cacheElement(XNode context) throws Exception {if (context != null) {// 獲取各種屬性String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);String eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);Long flushInterval = context.getLongAttribute("flushInterval");Integer size = context.getIntAttribute("size");boolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);// 獲取子節(jié)點(diǎn)配置Properties props = context.getChildrenAsProperties();// 構(gòu)建緩存對象builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);} }

    上面代碼中,大段代碼用來解析 <cache> 節(jié)點(diǎn)的屬性和子節(jié)點(diǎn),這些代碼沒什么好說的。緩存的構(gòu)建邏輯封裝在 BuilderAssistant 類的 useNewCache 方法中,下面我們來看一下該方法的邏輯。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // -☆- MapperBuilderAssistant public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {// 使用建造模式構(gòu)建緩存實(shí)例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();// 添加緩存到 Configuration 對象中configuration.addCache(cache);// 設(shè)置 currentCache 遍歷,即當(dāng)前使用的緩存currentCache = cache;return cache; }

    上面使用了建造模式構(gòu)建 Cache 實(shí)例,Cache 實(shí)例的構(gòu)建過程略為復(fù)雜,我們跟下去看看。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // -☆- CacheBuilder public Cache build() {// 設(shè)置默認(rèn)的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)setDefaultImplementations();// 通過反射創(chuàng)建緩存Cache cache = newBaseCacheInstance(implementation, id);setCacheProperties(cache);// 僅對內(nèi)置緩存 PerpetualCache 應(yīng)用裝飾器if (PerpetualCache.class.equals(cache.getClass())) {// 遍歷裝飾器集合,應(yīng)用裝飾器for (Class<? extends Cache> decorator : decorators) {// 通過反射創(chuàng)建裝飾器實(shí)例cache = newCacheDecoratorInstance(decorator, cache);// 設(shè)置屬性值到緩存實(shí)例中setCacheProperties(cache);}// 應(yīng)用標(biāo)準(zhǔn)的裝飾器,比如 LoggingCache、SynchronizedCachecache = setStandardDecorators(cache);} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {// 應(yīng)用具有日志功能的緩存裝飾器cache = new LoggingCache(cache);}return cache; }

    上面的構(gòu)建過程流程較為復(fù)雜,這里總結(jié)一下。如下:

  • 設(shè)置默認(rèn)的緩存類型及裝飾器
  • 應(yīng)用裝飾器到 PerpetualCache 對象上
    • 遍歷裝飾器類型集合,并通過反射創(chuàng)建裝飾器實(shí)例
    • 將屬性設(shè)置到實(shí)例中
  • 應(yīng)用一些標(biāo)準(zhǔn)的裝飾器
  • 對非 LoggingCache 類型的緩存應(yīng)用 LoggingCache 裝飾器
  • 在以上4個步驟中,最后一步的邏輯很簡單,無需多說。下面按順序分析前3個步驟對應(yīng)的邏輯,如下:

    1 2 3 4 5 6 7 8 9 10 private void setDefaultImplementations() {if (implementation == null) {// 設(shè)置默認(rèn)的緩存實(shí)現(xiàn)類implementation = PerpetualCache.class;if (decorators.isEmpty()) {// 添加 LruCache 裝飾器decorators.add(LruCache.class);}} }

    以上邏輯比較簡單,主要做的事情是在 implementation 為空的情況下,為它設(shè)置一個默認(rèn)值。如果大家仔細(xì)看前面的方法,會發(fā)現(xiàn) MyBatis 做了不少判空的操作。比如:

    1 2 3 4 5 6 7 8 9 10 // 判空操作1,若用戶未設(shè)置 cache 節(jié)點(diǎn)的 type 和 eviction 屬性,這里設(shè)置默認(rèn)值 PERPETUAL String type = context.getStringAttribute("type", "PERPETUAL"); String eviction = context.getStringAttribute("eviction", "LRU");// 判空操作2,若 typeClass 或 evictionClass 為空,valueOrDefault 方法會為它們設(shè)置默認(rèn)值 Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class))// 省略部分代碼.build();

    既然前面已經(jīng)做了兩次判空操作,implementation 不可能為空,那么 setDefaultImplementations 方法似乎沒有存在的必要了。其實(shí)不然,如果有人不按套路寫代碼。比如:

    1 2 3 Cache cache = new CacheBuilder(currentNamespace)// 忘記設(shè)置 implementation.build();

    這里忘記設(shè)置 implementation,或人為的將 implementation 設(shè)為空。如果不對 implementation 進(jìn)行判空,會導(dǎo)致 build 方法在構(gòu)建實(shí)例時觸發(fā)空指針異常,對于框架來說,出現(xiàn)空指針異常是很尷尬的,這是一個低級錯誤。這里以及之前做了這么多判空,就是為了避免出現(xiàn)空指針的情況,以提高框架的健壯性。好了,關(guān)于 setDefaultImplementations 方法的分析先到這,繼續(xù)往下分析。

    我們在使用 MyBatis 內(nèi)置緩存時,一般不用為它們配置自定義屬性。但使用第三方緩存時,則應(yīng)按需進(jìn)行配置。比如前面演示 MyBatis 整合 Ehcache 時,就為 Ehcache 配置了一些必要的屬性。下面我們來看一下這部分配置是如何設(shè)置到緩存實(shí)例中的。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 private void setCacheProperties(Cache cache) {if (properties != null) {/** 為緩存實(shí)例生成一個“元信息”實(shí)例,forObject 方法調(diào)用層次比較深,但最終調(diào)用了 * MetaClass 的 forClass 方法。關(guān)于 MetaClass 的源碼,我在上一篇文章中已經(jīng)* 詳細(xì)分析過了,這里不再贅述。*/MetaObject metaCache = SystemMetaObject.forObject(cache);for (Map.Entry<Object, Object> entry : properties.entrySet()) {String name = (String) entry.getKey();String value = (String) entry.getValue();if (metaCache.hasSetter(name)) {// 獲取 setter 方法的參數(shù)類型Class<?> type = metaCache.getSetterType(name);/** 根據(jù)參數(shù)類型對屬性值進(jìn)行轉(zhuǎn)換,并將轉(zhuǎn)換后的值* 通過 setter 方法設(shè)置到 Cache 實(shí)例中*/if (String.class == type) {metaCache.setValue(name, value);} else if (int.class == type || Integer.class == type) {/** 此處及以下分支包含兩個步驟:* 1.類型轉(zhuǎn)換 → Integer.valueOf(value)* 2.將轉(zhuǎn)換后的值設(shè)置到緩存實(shí)例中 → metaCache.setValue(name, value)*/ metaCache.setValue(name, Integer.valueOf(value));} else if (long.class == type || Long.class == type) {metaCache.setValue(name, Long.valueOf(value));} else if (short.class == type || Short.class == type) {...} else if (byte.class == type || Byte.class == type) {...} else if (float.class == type || Float.class == type) {...} else if (boolean.class == type || Boolean.class == type) {...} else if (double.class == type || Double.class == type) {...} else {throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);}}}}// 如果緩存類實(shí)現(xiàn)了 InitializingObject 接口,則調(diào)用 initialize 方法執(zhí)行初始化邏輯if (InitializingObject.class.isAssignableFrom(cache.getClass())) {try {((InitializingObject) cache).initialize();} catch (Exception e) {throw new CacheException("Failed cache initialization for '" +cache.getId() + "' on '" + cache.getClass().getName() + "'", e);}} }

    上面的大段代碼用于對屬性值進(jìn)行類型轉(zhuǎn)換,和設(shè)置轉(zhuǎn)換后的值到 Cache 實(shí)例中。關(guān)于上面代碼中出現(xiàn)的 MetaObject,大家可以自己嘗試分析一下。最后,我們來看一下設(shè)置標(biāo)準(zhǔn)裝飾器的過程。如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private Cache setStandardDecorators(Cache cache) {try {// 創(chuàng)建“元信息”對象MetaObject metaCache = SystemMetaObject.forObject(cache);if (size != null && metaCache.hasSetter("size")) {// 設(shè)置 size 屬性,metaCache.setValue("size", size);}if (clearInterval != null) {// clearInterval 不為空,應(yīng)用 ScheduledCache 裝飾器cache = new ScheduledCache(cache);((ScheduledCache) cache).setClearInterval(clearInterval);}if (readWrite) {// readWrite 為 true,應(yīng)用 SerializedCache 裝飾器cache = new SerializedCache(cache);}/** 應(yīng)用 LoggingCache,SynchronizedCache 裝飾器,* 使原緩存具備打印日志和線程同步的能力*/cache = new LoggingCache(cache);cache = new SynchronizedCache(cache);if (blocking) {// blocking 為 true,應(yīng)用 BlockingCache 裝飾器cache = new BlockingCache(cache);}return cache;} catch (Exception e) {throw new CacheException("Error building standard cache decorators. Cause: " + e, e);} }

    以上代碼用于為緩存應(yīng)用一些基本的裝飾器,除了 LoggingCache 和 SynchronizedCache 這兩個是必要的裝飾器,其他的裝飾器應(yīng)用與否,取決于用戶的配置。

    到此,關(guān)于緩存的解析過程就分析完了。這一塊的內(nèi)容比較多,不過好在代碼邏輯不是很復(fù)雜,耐心看還是可以弄懂的。其他的就不多說了,進(jìn)入下一節(jié)的分析。

    ?2.1.2 解析 <cache-ref> 節(jié)點(diǎn)

    在 MyBatis 中,二級緩存是可以共用的。這需要使用 <cache-ref> 節(jié)點(diǎn)配置參照緩存,比如像下面這樣。

    1 2 3 4 5 6 7 8 9 10 <!-- Mapper1.xml --> <mapper namespace="xyz.coolblog.dao.Mapper1"><!-- Mapper1 與 Mapper2 共用一個二級緩存 --><cache-ref namespace="xyz.coolblog.dao.Mapper2"/> </mapper><!-- Mapper2.xml --> <mapper namespace="xyz.coolblog.dao.Mapper2"><cache/> </mapper>

    接下來,我們對照上面的配置分析 cache-ref 的解析過程。如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void cacheRefElement(XNode context) {if (context != null) {configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));// 創(chuàng)建 CacheRefResolver 實(shí)例CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));try {// 解析參照緩存cacheRefResolver.resolveCacheRef();} catch (IncompleteElementException e) {/** 這里對 IncompleteElementException 異常進(jìn)行捕捉,并將 cacheRefResolver * 存入到 Configuration 的 incompleteCacheRefs 集合中*/configuration.addIncompleteCacheRef(cacheRefResolver);}} }

    如上所示,<cache-ref> 節(jié)點(diǎn)的解析邏輯封裝在了 CacheRefResolver 的 resolveCacheRef 方法中。下面,我們一起看一下這個方法的邏輯。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // -☆- CacheRefResolver public Cache resolveCacheRef() {// 調(diào)用 builderAssistant 的 useNewCache(namespace) 方法return assistant.useCacheRef(cacheRefNamespace); }// -☆- MapperBuilderAssistant public Cache useCacheRef(String namespace) {if (namespace == null) {throw new BuilderException("cache-ref element requires a namespace attribute.");}try {unresolvedCacheRef = true;// 根據(jù)命名空間從全局配置對象(Configuration)中查找相應(yīng)的緩存實(shí)例Cache cache = configuration.getCache(namespace);/** 若未查找到緩存實(shí)例,此處拋出異常。這里存在兩種情況導(dǎo)致未查找到 cache 實(shí)例,* 分別如下:* 1.使用者在 <cache-ref> 中配置了一個不存在的命名空間,* 導(dǎo)致無法找到 cache 實(shí)例* 2.使用者所引用的緩存實(shí)例還未創(chuàng)建*/if (cache == null) {throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");}// 設(shè)置 cache 為當(dāng)前使用緩存currentCache = cache;unresolvedCacheRef = false;return cache;} catch (IllegalArgumentException e) {throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);} }

    以上是 cache-ref 的解析過程,邏輯并不復(fù)雜。不過這里要注意 cache 為空的情況,我在代碼中已經(jīng)注釋了可能導(dǎo)致 cache 為空的兩種情況。第一種情況比較好理解,第二種情況稍微復(fù)雜點(diǎn),但是也不難理解。我會在 2.3 節(jié)進(jìn)行解釋說明,這里先不說。

    到此,關(guān)于 <cache-ref> 節(jié)點(diǎn)的解析過程就分析完了。本節(jié)的內(nèi)容不是很難理解,就不多說了。

    ?2.1.3 解析 <resultMap> 節(jié)點(diǎn)

    resultMap 是 MyBatis 框架中常用的特性,主要用于映射結(jié)果。resultMap 是 MyBatis 提供的一個強(qiáng)力武器,這一點(diǎn)官方文檔中有所描述,這里引用一下。

    resultMap 元素是 MyBatis 中最重要最強(qiáng)大的元素。它可以讓你從 90% 的 JDBC ResultSets 數(shù)據(jù)提取代碼中解放出來, 并在一些情形下允許你做一些 JDBC 不支持的事情。 實(shí)際上,在對復(fù)雜語句進(jìn)行聯(lián)合映射的時候,它很可能可以代替數(shù)千行的同等功能的代碼。 ResultMap 的設(shè)計(jì)思想是,簡單的語句不需要明確的結(jié)果映射,而復(fù)雜一點(diǎn)的語句只需要描述它們的關(guān)系就行了。

    如上描述,resultMap 元素是 MyBatis 中最重要最強(qiáng)大的元素,它可以把大家從 JDBC ResultSets 數(shù)據(jù)提取的工作中解放出來。通過 resultMap 和自動映射,可以讓 MyBatis 幫助我們完成 ResultSet → Object 的映射,這將會大大提高了開發(fā)效率。關(guān)于 resultMap 的用法,我相信大家都比較熟悉了,所以這里我就不介紹了。當(dāng)然,如果大家不熟悉也沒關(guān)系,MyBatis 的官方文檔上對此進(jìn)行了詳細(xì)的介紹,大家不妨去看看。

    好了,其他的就不多說了,下面開始分析 resultMap 配置的解析過程。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 // -☆- XMLMapperBuilder private void resultMapElements(List<XNode> list) throws Exception {// 遍歷 <resultMap> 節(jié)點(diǎn)列表for (XNode resultMapNode : list) {try {// 解析 resultMap 節(jié)點(diǎn)resultMapElement(resultMapNode);} catch (IncompleteElementException e) {// ignore, it will be retried}} }private ResultMap resultMapElement(XNode resultMapNode) throws Exception {// 調(diào)用重載方法return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList()); }private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());// 獲取 id 和 type 屬性String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());String type = resultMapNode.getStringAttribute("type",resultMapNode.getStringAttribute("ofType",resultMapNode.getStringAttribute("resultType",resultMapNode.getStringAttribute("javaType"))));// 獲取 extends 和 autoMappingString extend = resultMapNode.getStringAttribute("extends");Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");// 解析 type 屬性對應(yīng)的類型Class<?> typeClass = resolveClass(type);Discriminator discriminator = null;List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();resultMappings.addAll(additionalResultMappings);// 獲取并遍歷 <resultMap> 的子節(jié)點(diǎn)列表List<XNode> resultChildren = resultMapNode.getChildren();for (XNode resultChild : resultChildren) {if ("constructor".equals(resultChild.getName())) {// 解析 constructor 節(jié)點(diǎn),并生成相應(yīng)的 ResultMappingprocessConstructorElement(resultChild, typeClass, resultMappings);} else if ("discriminator".equals(resultChild.getName())) {// 解析 discriminator 節(jié)點(diǎn)discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);} else {List<ResultFlag> flags = new ArrayList<ResultFlag>();if ("id".equals(resultChild.getName())) {// 添加 ID 到 flags 集合中flags.add(ResultFlag.ID);}// 解析 id 和 property 節(jié)點(diǎn),并生成相應(yīng)的 ResultMappingresultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}}ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,discriminator, resultMappings, autoMapping);try {// 根據(jù)前面獲取到的信息構(gòu)建 ResultMap 對象return resultMapResolver.resolve();} catch (IncompleteElementException e) {/** 如果發(fā)生 IncompleteElementException 異常,* 這里將 resultMapResolver 添加到 incompleteResultMaps 集合中*/ configuration.addIncompleteResultMap(resultMapResolver);throw e;} }

    上面的代碼比較多,看起來有點(diǎn)復(fù)雜,這里總結(jié)一下:

  • 獲取 <resultMap> 節(jié)點(diǎn)的各種屬性
  • 遍歷 <resultMap> 的子節(jié)點(diǎn),并根據(jù)子節(jié)點(diǎn)名稱執(zhí)行相應(yīng)的解析邏輯
  • 構(gòu)建 ResultMap 對象
  • 若構(gòu)建過程中發(fā)生異常,則將 resultMapResolver 添加到 incompleteResultMaps 集合中
  • 如上流程,第1步和最后一步都是一些常規(guī)操作,無需過多解釋。第2步和第3步則是接下來需要重點(diǎn)分析的操作,這其中,鑒別器 discriminator 不是很常用的特性,我覺得大家知道它有什么用就行了,所以就不分析了。下面先來分析 <id> 和 <result> 節(jié)點(diǎn)的解析邏輯。

    ?2.1.3.1 解析 <id> 和 <result> 節(jié)點(diǎn)

    在 <resultMap> 節(jié)點(diǎn)中,子節(jié)點(diǎn) <id> 和 <result> 都是常規(guī)配置,比較常見。相信大家對此也比較熟悉了,我就不多說了。下面我們直接分析這兩個節(jié)點(diǎn)的解析過程。如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {String property;// 根據(jù)節(jié)點(diǎn)類型獲取 name 或 property 屬性if (flags.contains(ResultFlag.CONSTRUCTOR)) {property = context.getStringAttribute("name");} else {property = context.getStringAttribute("property");}// 獲取其他各種屬性String column = context.getStringAttribute("column");String javaType = context.getStringAttribute("javaType");String jdbcType = context.getStringAttribute("jdbcType");String nestedSelect = context.getStringAttribute("select");/** 解析 resultMap 屬性,該屬性出現(xiàn)在 <association> 和 <collection> 節(jié)點(diǎn)中。* 若這兩個節(jié)點(diǎn)不包含 resultMap 屬性,則調(diào)用 processNestedResultMappings 方法* 解析嵌套 resultMap。*/String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));String notNullColumn = context.getStringAttribute("notNullColumn");String columnPrefix = context.getStringAttribute("columnPrefix");String typeHandler = context.getStringAttribute("typeHandler");String resultSet = context.getStringAttribute("resultSet");String foreignColumn = context.getStringAttribute("foreignColumn");boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));// 解析 javaType、typeHandler 的類型以及枚舉類型 JdbcTypeClass<?> javaTypeClass = resolveClass(javaType);Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);// 構(gòu)建 ResultMapping 對象return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect,nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); }

    上面的方法主要用于獲取 <id> 和 <result> 節(jié)點(diǎn)的屬性,其中,resultMap 屬性的解析過程要相對復(fù)雜一些。該屬性存在于 <association> 和 <collection> 節(jié)點(diǎn)中。下面以 <association> 節(jié)點(diǎn)為例,演示該節(jié)點(diǎn)的兩種配置方式,分別如下:

    第一種配置方式是通過 resultMap 屬性引用其他的 <resultMap> 節(jié)點(diǎn),配置如下:

    1 2 3 4 5 6 7 8 9 10 11 <resultMap id="articleResult" type="Article"><id property="id" column="id"/><result property="title" column="article_title"/><!-- 引用 authorResult --><association property="article_author" column="article_author_id" javaType="Author" resultMap="authorResult"/> </resultMap><resultMap id="authorResult" type="Author"><id property="id" column="author_id"/><result property="name" column="author_name"/> </resultMap>

    第二種配置方式是采取 resultMap 嵌套的方式進(jìn)行配置,如下:

    1 2 3 4 5 6 7 8 9 <resultMap id="articleResult" type="Article"><id property="id" column="id"/><result property="title" column="article_title"/><!-- resultMap 嵌套 --><association property="article_author" javaType="Author"><id property="id" column="author_id"/><result property="name" column="author_name"/></association> </resultMap>

    如上配置,<association> 的子節(jié)點(diǎn)是一些結(jié)果映射配置,這些結(jié)果配置最終也會被解析成 ResultMap。我們可以看看解析過程是怎樣的,如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {// 判斷節(jié)點(diǎn)名稱if ("association".equals(context.getName())|| "collection".equals(context.getName())|| "case".equals(context.getName())) {if (context.getStringAttribute("select") == null) {// resultMapElement 是解析 ResultMap 入口方法ResultMap resultMap = resultMapElement(context, resultMappings);// 返回 resultMap idreturn resultMap.getId();}}return null; }

    如上,<association> 的子節(jié)點(diǎn)由 resultMapElement 方法解析成 ResultMap,并在最后返回?resultMap.id。對于 <resultMap> 節(jié)點(diǎn),id 的值配置在該節(jié)點(diǎn)的 id 屬性中。但 <association> 節(jié)點(diǎn)無法配置 id 屬性,那么該 id 如何產(chǎn)生的呢?答案在 XNode 類的 getValueBasedIdentifier 方法中,這個方法具體邏輯我就不分析了。下面直接看一下以上配置中的 <association> 節(jié)點(diǎn)解析成 ResultMap 后的 id 值,如下:

    1 id = mapper_resultMap[articleResult]_association[article_author]

    關(guān)于嵌套 resultMap 的解析邏輯就先分析到這,下面分析 ResultMapping 的構(gòu)建過程。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn, boolean lazy) {/** 若 javaType 為空,這里根據(jù) property 的屬性進(jìn)行解析。關(guān)于下面方法中的參數(shù),* 這里說明一下:* - resultType:即 <resultMap type="xxx"/> 中的 type 屬性* - property:即 <result property="xxx"/> 中的 property 屬性*/Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);// 解析 TypeHandlerTypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);/** 解析 column = {property1=column1, property2=column2} 的情況,* 這里會將 column 拆分成多個 ResultMapping*/List<ResultMapping> composites = parseCompositeColumnName(column);// 通過建造模式構(gòu)建 ResultMappingreturn new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType).nestedQueryId(applyCurrentNamespace(nestedSelect, true)).nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet).typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<ResultFlag>() : flags).composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn)).columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).build(); }// -☆- ResultMapping.Builder public ResultMapping build() {// 將 flags 和 composites 兩個集合變?yōu)椴豢尚薷募蟫esultMapping.flags = Collections.unmodifiableList(resultMapping.flags);resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);// 從 TypeHandlerRegistry 中獲取相應(yīng) TypeHandlerresolveTypeHandler();validate();return resultMapping; }

    ResultMapping 的構(gòu)建過程不是很復(fù)雜,首先是解析 javaType 類型,并創(chuàng)建 typeHandler 實(shí)例。然后處理復(fù)合 column。最后通過建造器構(gòu)建 ResultMapping 實(shí)例。關(guān)于上面方法中出現(xiàn)的一些方法調(diào)用,這里接不跟下去分析了,大家可以自己看看。

    到此關(guān)于 ResultMapping 的解析和構(gòu)建過程就分析完了,總的來說,還是比較復(fù)雜的。不過再難也是人寫的,靜下心都可以看懂。好了,其他就不多說了,繼續(xù)往下分析。

    ?2.1.3.2 解析 <constructor> 節(jié)點(diǎn)

    一般情況下,我們所定義的實(shí)體類都是簡單的 Java 對象,即 POJO。這種對象包含一些私有屬性和相應(yīng)的 getter/setter 方法,通常這種 POJO 可以滿足大部分需求。但如果你想使用不可變類存儲查詢結(jié)果,則就需要做一些改動。比如把 POJO 的 setter 方法移除,增加構(gòu)造方法用于初始化成員變量。對于這種不可變的 Java 類,需要通過帶有參數(shù)的構(gòu)造方法進(jìn)行初始化(反射也可以達(dá)到同樣目的)。下面舉個例子說明一下:

    1 2 3 4 5 6 7 8 9 10 11 12 public class ArticleDO {// ...public ArticleDO(Integer id, String title, String content) {this.id = id;this.title = title;this.content = content;}// ... }

    如上,ArticleDO 的構(gòu)造方法對應(yīng)的配置如下:

    1 2 3 4 5 <constructor><idArg column="id" name="id"/><arg column="title" name="title"/><arg column="content" name="content"/> </constructor>

    下面,分析 constructor 節(jié)點(diǎn)的解析過程。如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {// 獲取子節(jié)點(diǎn)列表List<XNode> argChildren = resultChild.getChildren();for (XNode argChild : argChildren) {List<ResultFlag> flags = new ArrayList<ResultFlag>();// 向 flags 中添加 CONSTRUCTOR 標(biāo)志flags.add(ResultFlag.CONSTRUCTOR);if ("idArg".equals(argChild.getName())) {// 向 flags 中添加 ID 標(biāo)志flags.add(ResultFlag.ID);}// 構(gòu)建 ResultMapping,上一節(jié)已經(jīng)分析過resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));} }

    如上,上面方法的邏輯并不復(fù)雜。首先是獲取并遍歷子節(jié)點(diǎn)列表,然后為每個子節(jié)點(diǎn)創(chuàng)建 flags 集合,并添加 CONSTRUCTOR 標(biāo)志。對于 idArg 節(jié)點(diǎn),額外添加 ID 標(biāo)志。最后一步則是構(gòu)建 ResultMapping,該步邏輯前面已經(jīng)分析過,這里就不多說了。

    分析完 <resultMap> 的子節(jié)點(diǎn) <id>,<result> 以及 <constructor> 的解析過程,下面來看看 ResultMap 實(shí)例的構(gòu)建過程。

    ?2.1.3.3 ResultMap 對象構(gòu)建過程分析

    前面用了不少的篇幅來分析 <resultMap> 子節(jié)點(diǎn)的解析過程。通過前面的分析,我們可知 <id>,<result> 等節(jié)點(diǎn)最終都被解析成了 ResultMapping。在得到這些 ResultMapping 后,緊接著要做的事情是構(gòu)建 ResultMap。如果說 ResultMapping 與單條結(jié)果映射相對應(yīng),那 ResultMap 與什么對應(yīng)呢?答案是…。答案暫時還不能說,我們到源碼中去找尋吧。下面,讓我們帶著這個疑問開始本節(jié)的源碼分析。

    前面分析了很多源碼,大家可能都忘了 ResultMap 構(gòu)建的入口了。這里再貼一下,如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {// 獲取 resultMap 節(jié)點(diǎn)中的屬性// ...// 解析 resultMap 對應(yīng)的類型// ...// 遍歷 resultMap 節(jié)點(diǎn)的子節(jié)點(diǎn),構(gòu)建 ResultMapping 對象// ...// 創(chuàng)建 ResultMap 解析器ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,discriminator, resultMappings, autoMapping);try {// 根據(jù)前面獲取到的信息構(gòu)建 ResultMap 對象return resultMapResolver.resolve();} catch (IncompleteElementException e) {configuration.addIncompleteResultMap(resultMapResolver);throw e;} }

    如上,ResultMap 的構(gòu)建邏輯分裝在 ResultMapResolver 的 resolve 方法中,下面我從該方法進(jìn)行分析。

    1 2 3 4 // -☆- ResultMapResolver public ResultMap resolve() {return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); }

    上面的方法將構(gòu)建 ResultMap 實(shí)例的任務(wù)委托給了 MapperBuilderAssistant 的 addResultMap,我們跟進(jìn)到這個方法中看看。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // -☆- MapperBuilderAssistant public ResultMap addResultMap(String id, Class<?> type, String extend, Discriminator discriminator,List<ResultMapping> resultMappings, Boolean autoMapping) {// 為 ResultMap 的 id 和 extend 屬性值拼接命名空間id = applyCurrentNamespace(id, false);extend = applyCurrentNamespace(extend, true);if (extend != null) {if (!configuration.hasResultMap(extend)) {throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");}ResultMap resultMap = configuration.getResultMap(extend);List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());// 為拓展 ResultMappings 取出重復(fù)項(xiàng)extendedResultMappings.removeAll(resultMappings);boolean declaresConstructor = false;// 檢測當(dāng)前 resultMappings 集合中是否包含 CONSTRUCTOR 標(biāo)志的元素for (ResultMapping resultMapping : resultMappings) {if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {declaresConstructor = true;break;}}/** 如果當(dāng)前 <resultMap> 節(jié)點(diǎn)中包含 <constructor> 子節(jié)點(diǎn),* 則將拓展 ResultMapping 集合中的包含 CONSTRUCTOR 標(biāo)志的元素移除*/if (declaresConstructor) {Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();while (extendedResultMappingsIter.hasNext()) {if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {extendedResultMappingsIter.remove();}}}// 將擴(kuò)展 resultMappings 集合合并到當(dāng)前 resultMappings 集合中resultMappings.addAll(extendedResultMappings);}// 構(gòu)建 ResultMapResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build();configuration.addResultMap(resultMap);return resultMap; }

    上面的方法主要用于處理 resultMap 節(jié)點(diǎn)的 extend 屬性,extend 不為空的話,這里將當(dāng)前 resultMappings 集合和擴(kuò)展 resultMappings 集合合二為一。隨后,通過建造模式構(gòu)建 ResultMap 實(shí)例。過程如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 // -☆- ResultMap public ResultMap build() {if (resultMap.id == null) {throw new IllegalArgumentException("ResultMaps must have an id");}resultMap.mappedColumns = new HashSet<String>();resultMap.mappedProperties = new HashSet<String>();resultMap.idResultMappings = new ArrayList<ResultMapping>();resultMap.constructorResultMappings = new ArrayList<ResultMapping>();resultMap.propertyResultMappings = new ArrayList<ResultMapping>();final List<String> constructorArgNames = new ArrayList<String>();for (ResultMapping resultMapping : resultMap.resultMappings) {/** 檢測 <association> 或 <collection> 節(jié)點(diǎn)* 是否包含 select 和 resultMap 屬性*/resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;resultMap.hasNestedResultMaps =resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);final String column = resultMapping.getColumn();if (column != null) {// 將 colum 轉(zhuǎn)換成大寫,并添加到 mappedColumns 集合中resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));} else if (resultMapping.isCompositeResult()) {for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {final String compositeColumn = compositeResultMapping.getColumn();if (compositeColumn != null) {resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));}}}// 添加屬性 property 到 mappedProperties 集合中final String property = resultMapping.getProperty();if (property != null) {resultMap.mappedProperties.add(property);}// 檢測當(dāng)前 resultMapping 是否包含 CONSTRUCTOR 標(biāo)志if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {// 添加 resultMapping 到 constructorResultMappings 中resultMap.constructorResultMappings.add(resultMapping);// 添加屬性(constructor 節(jié)點(diǎn)的 name 屬性)到 constructorArgNames 中if (resultMapping.getProperty() != null) {constructorArgNames.add(resultMapping.getProperty());}} else {// 添加 resultMapping 到 propertyResultMappings 中resultMap.propertyResultMappings.add(resultMapping);}if (resultMapping.getFlags().contains(ResultFlag.ID)) {// 添加 resultMapping 到 idResultMappings 中resultMap.idResultMappings.add(resultMapping);}}if (resultMap.idResultMappings.isEmpty()) {resultMap.idResultMappings.addAll(resultMap.resultMappings);}if (!constructorArgNames.isEmpty()) {// 獲取構(gòu)造方法參數(shù)列表,篇幅原因,這個方法不分析了final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);if (actualArgNames == null) {throw new BuilderException("Error in result map '" + resultMap.id+ "'. Failed to find a constructor in '"+ resultMap.getType().getName() + "' by arg names " + constructorArgNames+ ". There might be more info in debug log.");}// 對 constructorResultMappings 按照構(gòu)造方法參數(shù)列表的順序進(jìn)行排序Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {@Overridepublic int compare(ResultMapping o1, ResultMapping o2) {int paramIdx1 = actualArgNames.indexOf(o1.getProperty());int paramIdx2 = actualArgNames.indexOf(o2.getProperty());return paramIdx1 - paramIdx2;}});}// 將以下這些集合變?yōu)椴豢尚薷募蟫esultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);return resultMap; }

    以上代碼看起來很復(fù)雜,實(shí)際上這是假象。以上代碼主要做的事情就是將 ResultMapping 實(shí)例及屬性分別存儲到不同的集合中,僅此而已。ResultMap 中定義了五種不同的集合,下面分別介紹一下這幾種集合。

    集合名稱用途
    mappedColumns用于存儲 <id>、<result>、<idArg>、<arg> 節(jié)點(diǎn)?column 屬性
    mappedProperties用于存儲 <id> 和 <result> 節(jié)點(diǎn)的 property 屬性,或 <idArgs> 和 <arg> 節(jié)點(diǎn)的?name 屬性
    idResultMappings用于存儲 <id> 和 <idArg> 節(jié)點(diǎn)對應(yīng)的?ResultMapping 對象
    propertyResultMappings用于存儲 <id> 和 <result>?節(jié)點(diǎn)對應(yīng)的 ResultMapping 對象
    constructorResultMappings用于存儲?<idArgs> 和 <arg> 節(jié)點(diǎn)對應(yīng)的?ResultMapping 對象

    上面干巴巴的描述不夠直觀。下面我們寫點(diǎn)代碼測試一下,并把這些集合的內(nèi)容打印到控制臺上,大家直觀感受一下。先定義一個映射文件,如下:

    1 2 3 4 5 6 7 8 9 10 11 12 <mapper namespace="xyz.coolblog.dao.ArticleDao"><resultMap id="articleResult" type="xyz.coolblog.model.Article"><constructor><idArg column="id" name="id"/><arg column="title" name="title"/><arg column="content" name="content"/></constructor><id property="id" column="id"/><result property="author" column="author"/><result property="createTime" column="create_time"/></resultMap> </mapper>

    測試代碼如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ResultMapTest {@Testpublic void printResultMapInfo() throws Exception {Configuration configuration = new Configuration();String resource = "mapper/ArticleMapper.xml";InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());builder.parse();ResultMap resultMap = configuration.getResultMap("articleResult");System.out.println("\n-------------------+? mappedColumns ?+--------------------");System.out.println(resultMap.getMappedColumns());System.out.println("\n------------------+? mappedProperties ?+------------------");System.out.println(resultMap.getMappedProperties());System.out.println("\n------------------+? idResultMappings ?+------------------");resultMap.getIdResultMappings().forEach(rm -> System.out.println(simplify(rm)));System.out.println("\n---------------+? propertyResultMappings ?+---------------");resultMap.getPropertyResultMappings().forEach(rm -> System.out.println(simplify(rm)));System.out.println("\n-------------+? constructorResultMappings ?+--------------");resultMap.getConstructorResultMappings().forEach(rm -> System.out.println(simplify(rm)));System.out.println("\n-------------------+? resultMappings ?+-------------------");resultMap.getResultMappings().forEach(rm -> System.out.println(simplify(rm)));inputStream.close();}/** 簡化 ResultMapping 輸出結(jié)果 */private String simplify(ResultMapping resultMapping) {return String.format("ResultMapping{column='%s', property='%s', flags=%s, ...}",resultMapping.getColumn(), resultMapping.getProperty(), resultMapping.getFlags());} }

    這里,我們把5個集合轉(zhuǎn)給你的內(nèi)容都打印出來,結(jié)果如下:

    如上,結(jié)果比較清晰明了,不需要過多解釋了。我們參照上面配置文件及輸出的結(jié)果,把 ResultMap 的大致輪廓畫出來。如下:

    到這里,<resultMap> 節(jié)點(diǎn)的解析過程就分析完了??偟膩碚f,該節(jié)點(diǎn)的解析過程還是比較復(fù)雜的。好了,其他的就不多說了,繼續(xù)后面的分析。

    ?2.1.4 解析 <sql> 節(jié)點(diǎn)

    <sql> 節(jié)點(diǎn)用來定義一些可重用的 SQL 語句片段,比如表名,或表的列名等。在映射文件中,我們可以通過 <include> 節(jié)點(diǎn)引用 <sql> 節(jié)點(diǎn)定義的內(nèi)容。下面我來演示一下 <sql> 節(jié)點(diǎn)的使用方式,如下:

    1 2 3 4 5 6 7 8 9 10 11 <sql id="table">article </sql><select id="findOne" resultType="Article">SELECT id, title FROM <include refid="table"/> WHERE id = #{id} </select><update id="update" parameterType="Article">UPDATE <include refid="table"/> SET title = #{title} WHERE id = #{id} </update>

    如上,上面配置中,<select> 和 <update> 節(jié)點(diǎn)通過 <include> 引入定義在 <sql> 節(jié)點(diǎn)中的表名。上面的配置比較常規(guī),除了靜態(tài)文本,<sql> 節(jié)點(diǎn)還支持屬性占位符 ${}。比如:

    1 2 3 <sql id="table">${table_prefix}_article </sql>

    如果屬性 table_prefix = blog,那么 <sql> 節(jié)點(diǎn)中的內(nèi)容最終為 blog_article。

    上面介紹了 <sql> 節(jié)點(diǎn)的用法,比較容易。下面分析一下 sql 節(jié)點(diǎn)的解析過程,如下:

    1 2 3 4 5 6 7 8 9 private void sqlElement(List<XNode> list) throws Exception {if (configuration.getDatabaseId() != null) {// 調(diào)用 sqlElement 解析 <sql> 節(jié)點(diǎn)sqlElement(list, configuration.getDatabaseId());}// 再次調(diào)用 sqlElement,不同的是,這次調(diào)用,該方法的第二個參數(shù)為 nullsqlElement(list, null); }

    這個方法需要大家注意一下,如果 Configuration 的 databaseId 不為空,sqlElement 方法會被調(diào)用了兩次。第一次傳入具體的 databaseId,用于解析帶有 databaseId 屬性,且屬性值與此相等的 <sql> 節(jié)點(diǎn)。第二次傳入的 databaseId 為空,用于解析未配置 databaseId 屬性的 <sql> 節(jié)點(diǎn)。這里是個小細(xì)節(jié),大家注意一下就好。我們繼續(xù)往下分析。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {for (XNode context : list) {// 獲取 id 和 databaseId 屬性String databaseId = context.getStringAttribute("databaseId");String id = context.getStringAttribute("id");// id = currentNamespace + "." + idid = builderAssistant.applyCurrentNamespace(id, false);// 檢測當(dāng)前 databaseId 和 requiredDatabaseId 是否一致if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {// 將 <id, XNode> 鍵值對緩存到 sqlFragments 中sqlFragments.put(id, context);}} }

    這個方法邏輯比較簡單,首先是獲取 <sql> 節(jié)點(diǎn)的 id 和 databaseId 屬性,然后為 id 屬性值拼接命名空間。最后,通過檢測當(dāng)前 databaseId 和 requiredDatabaseId 是否一致,來決定保存還是忽略當(dāng)前的 <sql> 節(jié)點(diǎn)。下面,我們來看一下 databaseId 的匹配邏輯是怎樣的。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {if (requiredDatabaseId != null) {// 當(dāng)前 databaseId 和目標(biāo) databaseId 不一致時,返回 falseif (!requiredDatabaseId.equals(databaseId)) {return false;}} else {// 如果目標(biāo) databaseId 為空,但當(dāng)前 databaseId 不為空。兩者不一致,返回 falseif (databaseId != null) {return false;}/** 如果當(dāng)前 <sql> 節(jié)點(diǎn)的 id 與之前的 <sql> 節(jié)點(diǎn)重復(fù),且先前節(jié)點(diǎn) * databaseId 不為空。則忽略當(dāng)前節(jié)點(diǎn),并返回 false*/if (this.sqlFragments.containsKey(id)) {XNode context = this.sqlFragments.get(id);if (context.getStringAttribute("databaseId") != null) {return false;}}}return true; }

    下面總結(jié)一下 databaseId 的匹配規(guī)則。

  • databaseId 與 requiredDatabaseId 不一致,即失配,返回 false
  • 當(dāng)前節(jié)點(diǎn)與之前的節(jié)點(diǎn)出現(xiàn) id 重復(fù)的情況,若之前的 <sql> 節(jié)點(diǎn) databaseId 屬性不為空,返回 false
  • 若以上兩條規(guī)則均匹配失敗,此時返回 true
  • 在上面三條匹配規(guī)則中,第二條規(guī)則稍微難理解一點(diǎn)。這里簡單分析一下,考慮下面這種配置。

    1 2 3 4 5 6 7 8 9 <!-- databaseId 不為空 --> <sql id="table" databaseId="mysql">article </sql><!-- databaseId 為空 --> <sql id="table">article </sql>

    在上面配置中,兩個 <sql> 節(jié)點(diǎn)的 id 屬性值相同,databaseId 屬性不一致。假設(shè) configuration.databaseId = mysql,第一次調(diào)用 sqlElement 方法,第一個 <sql> 節(jié)點(diǎn)對應(yīng)的 XNode 會被放入到 sqlFragments 中。第二次調(diào)用 sqlElement 方法時,requiredDatabaseId 參數(shù)為空。由于 sqlFragments 中已包含了一個 id 節(jié)點(diǎn),且該節(jié)點(diǎn)的 databaseId 不為空,此時匹配邏輯返回 false,第二個節(jié)點(diǎn)不會被保存到 sqlFragments。

    上面的分析內(nèi)容涉及到了 databaseId,關(guān)于 databaseId 的用途,這里簡單介紹一下。databaseId 用于標(biāo)明數(shù)據(jù)庫廠商的身份,不同廠商有自己的 SQL 方言,MyBatis 可以根據(jù) databaseId 執(zhí)行不同 SQL 語句。databaseId 在 <sql> 節(jié)點(diǎn)中有什么用呢?這個問題也不難回答。<sql> 節(jié)點(diǎn)用于保存 SQL 語句片段,如果 SQL 語句片段中包含方言的話,那么該 <sql> 節(jié)點(diǎn)只能被同一 databaseId 的查詢語句或更新語句引用。關(guān)于 databaseId,這里就介紹這么多。

    好了,本節(jié)內(nèi)容先到這里。繼續(xù)往下分析。

    ?2.1.5 解析 SQL 語句節(jié)點(diǎn)

    前面分析了 <cache>、<cache-ref>、<resultMap> 以及 <sql> 節(jié)點(diǎn),從這一節(jié)開始,我們要分析映射文件中剩余的幾個節(jié)點(diǎn),分別是 <select>、<insert>、<update> 以及 <delete> 等。這幾個節(jié)點(diǎn)中存儲的是相同的內(nèi)容,都是 SQL 語句,所以這幾個節(jié)點(diǎn)的解析過程也是相同的。在進(jìn)行代碼分析之前,這里需要特別說明一下:為了避免和 <sql> 節(jié)點(diǎn)混淆,同時也為了描述方便,這里把 <select>、<insert>、<update> 以及 <delete> 等節(jié)點(diǎn)統(tǒng)稱為 SQL 語句節(jié)點(diǎn)。好了,下面開始本節(jié)的分析。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) {// 調(diào)用重載方法構(gòu)建 StatementbuildStatementFromContext(list, configuration.getDatabaseId());}// 調(diào)用重載方法構(gòu)建 Statement,requiredDatabaseId 參數(shù)為空buildStatementFromContext(list, null); }private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// 創(chuàng)建 Statement 建造類final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {/** 解析 Statement 節(jié)點(diǎn),并將解析結(jié)果存儲到 * configuration 的 mappedStatements 集合中*/statementParser.parseStatementNode();} catch (IncompleteElementException e) {// 解析失敗,將解析器放入 configuration 的 incompleteStatements 集合中configuration.addIncompleteStatement(statementParser);}} }

    上面的解析方法沒有什么實(shí)質(zhì)性的解析邏輯,我們繼續(xù)往下分析。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public void parseStatementNode() {// 獲取 id 和 databaseId 屬性String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");// 根據(jù) databaseId 進(jìn)行檢測,檢測邏輯和上一節(jié)基本一致,這里不再贅述if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 獲取各種屬性Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);String resultMap = context.getStringAttribute("resultMap");String resultType = context.getStringAttribute("resultType");String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);// 通過別名解析 resultType 對應(yīng)的類型Class<?> resultTypeClass = resolveClass(resultType);String resultSetType = context.getStringAttribute("resultSetType");// 解析 Statement 類型,默認(rèn)為 PREPAREDStatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 解析 ResultSetTypeResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);// 獲取節(jié)點(diǎn)的名稱,比如 <select> 節(jié)點(diǎn)名稱為 selectString nodeName = context.getNode().getNodeName();// 根據(jù)節(jié)點(diǎn)名稱解析 SqlCommandTypeSqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);boolean useCache = context.getBooleanAttribute("useCache", isSelect);boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// 解析 <include> 節(jié)點(diǎn)XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 解析 <selectKey> 節(jié)點(diǎn)processSelectKeyNodes(id, parameterTypeClass, langDriver);// 解析 SQL 語句SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);String resultSets = context.getStringAttribute("resultSets");String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {// 獲取 KeyGenerator 實(shí)例keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {// 創(chuàng)建 KeyGenerator 實(shí)例keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}/** 構(gòu)建 MappedStatement 對象,并將該對象存儲到 * Configuration 的 mappedStatements 集合中*/builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }

    上面的代碼比較長,看起來有點(diǎn)復(fù)雜。不過如果大家耐心看一下源碼,會發(fā)現(xiàn),上面的代碼中起碼有一般的代碼都是用來獲取節(jié)點(diǎn)屬性,以及解析部分屬性等。拋去這部分代碼,以上代碼做的事情如下。

  • 解析 <include> 節(jié)點(diǎn)
  • 解析 <selectKey> 節(jié)點(diǎn)
  • 解析 SQL,獲取 SqlSource
  • 構(gòu)建 MappedStatement 實(shí)例
  • 以上流程對應(yīng)的代碼比較復(fù)雜,每個步驟都能分析出一些東西來。下面我會每個步驟都進(jìn)行分析,首先來分析 <include> 節(jié)點(diǎn)的解析過程。

    ?2.1.5.1 解析 <include> 節(jié)點(diǎn)

    <include> 節(jié)點(diǎn)的解析邏輯封裝在 applyIncludes 中,該方法的代碼如下:

    1 2 3 4 5 6 7 8 9 10 11 public void applyIncludes(Node source) {Properties variablesContext = new Properties();Properties configurationVariables = configuration.getVariables();if (configurationVariables != null) {// 將 configurationVariables 中的數(shù)據(jù)添加到 variablesContext 中variablesContext.putAll(configurationVariables);}// 調(diào)用重載方法處理 <include> 節(jié)點(diǎn)applyIncludes(source, variablesContext, false); }

    上面代碼創(chuàng)建了一個新的 Properties 對象,并將全局 Properties 添加到其中。這樣做的原因是 applyIncludes 的重載方法會向 Properties 中添加新的元素,如果直接將全局 Properties 傳給重載方法,會造成全局 Properties 被污染。這是個小細(xì)節(jié),一般容易被忽視掉。其他沒什么需要注意的了,我們繼續(xù)往下看。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 private void applyIncludes(Node source, final Properties variablesContext, boolean included) {// ?? 第一個條件分支if (source.getNodeName().equals("include")) {/** 獲取 <sql> 節(jié)點(diǎn)。若 refid 中包含屬性占位符 ${},* 則需先將屬性占位符替換為對應(yīng)的屬性值*/Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);/** 解析 <include> 的子節(jié)點(diǎn) <property>,并將解析結(jié)果與 variablesContext 融合,* 然后返回融合后的 Properties。若 <property> 節(jié)點(diǎn)的 value 屬性中存在占位符 ${},* 則將占位符替換為對應(yīng)的屬性值*/Properties toIncludeContext = getVariablesContext(source, variablesContext);/** 這里是一個遞歸調(diào)用,用于將 <sql> 節(jié)點(diǎn)內(nèi)容中出現(xiàn)的屬性占位符 ${} 替換為對應(yīng)的* 屬性值。這里要注意一下遞歸調(diào)用的參數(shù):* * - toInclude:<sql> 節(jié)點(diǎn)對象* - toIncludeContext:<include> 子節(jié)點(diǎn) <property> 的解析結(jié)果與* 全局變量融合后的結(jié)果 */applyIncludes(toInclude, toIncludeContext, true);/** 如果 <sql> 和 <include> 節(jié)點(diǎn)不在一個文檔中,* 則從其他文檔中將 <sql> 節(jié)點(diǎn)引入到 <include> 所在文檔中*/if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {toInclude = source.getOwnerDocument().importNode(toInclude, true);}// 將 <include> 節(jié)點(diǎn)替換為 <sql> 節(jié)點(diǎn)source.getParentNode().replaceChild(toInclude, source);while (toInclude.hasChildNodes()) {// 將 <sql> 中的內(nèi)容插入到 <sql> 節(jié)點(diǎn)之前toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);}/** 前面已經(jīng)將 <sql> 節(jié)點(diǎn)的內(nèi)容插入到 dom 中了,* 現(xiàn)在不需要 <sql> 節(jié)點(diǎn)了,這里將該節(jié)點(diǎn)從 dom 中移除*/toInclude.getParentNode().removeChild(toInclude);// ?? 第二個條件分支} else if (source.getNodeType() == Node.ELEMENT_NODE) {if (included && !variablesContext.isEmpty()) {NamedNodeMap attributes = source.getAttributes();for (int i = 0; i < attributes.getLength(); i++) {Node attr = attributes.item(i);// 將 source 節(jié)點(diǎn)屬性中的占位符 ${} 替換成具體的屬性值attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));}}NodeList children = source.getChildNodes();for (int i = 0; i < children.getLength(); i++) {// 遞歸調(diào)用applyIncludes(children.item(i), variablesContext, included);}// ?? 第三個條件分支} else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {// 將文本(text)節(jié)點(diǎn)中的屬性占位符 ${} 替換成具體的屬性值source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));} }

    上面的代碼如果從上往下讀,不太容易看懂。因?yàn)樯厦娴姆椒ㄓ扇齻€條件分支,外加兩個遞歸調(diào)用組成,代碼的執(zhí)行順序并不是由上而下。要理解上面的代碼,我們需要定義一些配置,并將配置帶入到具體代碼中,逐行進(jìn)行演繹。不過,更推薦的方式是使用 IDE 進(jìn)行單步調(diào)試。為了便于講解,我把上面代碼中的三個分支都用 ?? 標(biāo)記了出來,這個大家注意一下。好了,必要的準(zhǔn)備工作做好了,下面開始演繹代碼的執(zhí)行過程。演繹所用的測試配置如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <mapper namespace="xyz.coolblog.dao.ArticleDao"><sql id="table">${table_name}</sql><select id="findOne" resultType="xyz.coolblog.dao.ArticleDO">SELECTid, titleFROM<include refid="table"><property name="table_name" value="article"/></include>WHERE id = #{id}</select> </mapper>

    我們先來看一下 applyIncludes 方法第一次被調(diào)用時的狀態(tài),如下:

    1 2 3 4 5 6 7 8 9 10 參數(shù)值: source = <select> 節(jié)點(diǎn) 節(jié)點(diǎn)類型:ELEMENT_NODE variablesContext = [ ] // 無內(nèi)容 included = false執(zhí)行流程: 1. 進(jìn)入條件分支2 2. 獲取 <select> 子節(jié)點(diǎn)列表 3. 遍歷子節(jié)點(diǎn)列表,將子節(jié)點(diǎn)作為參數(shù),進(jìn)行遞歸調(diào)用

    第一次調(diào)用 applyIncludes 方法,source = <select>,代碼進(jìn)入條件分支2。在該分支中,首先要獲取 <select> 節(jié)點(diǎn)的子節(jié)點(diǎn)列表。可獲取到的子節(jié)點(diǎn)如下:

    編號子節(jié)點(diǎn)類型描述
    1SELECT id, title FROMTEXT_NODE文本節(jié)點(diǎn)
    2<include refid=“table”/>ELEMENT_NODE普通節(jié)點(diǎn)
    3WHERE id = #{id}TEXT_NODE文本節(jié)點(diǎn)

    在獲取到子節(jié)點(diǎn)類列表后,接下來要做的事情是遍歷列表,然后將子節(jié)點(diǎn)作為參數(shù)進(jìn)行遞歸調(diào)用。在上面三個子節(jié)點(diǎn)中,子節(jié)點(diǎn)1和子節(jié)點(diǎn)3都是文本節(jié)點(diǎn),調(diào)用過程一致。因此,下面我只會演示子節(jié)點(diǎn)1和子節(jié)點(diǎn)2的遞歸調(diào)用過程。先來演示子節(jié)點(diǎn)1的調(diào)用過程,如下:

    節(jié)點(diǎn)1的調(diào)用過程比較簡單,只有兩層調(diào)用。然后我們在看一下子節(jié)點(diǎn)2的調(diào)用過程,如下:

    上面是子節(jié)點(diǎn)2的調(diào)用過程,共有四層調(diào)用,略為復(fù)雜。大家自己也對著配置,把源碼走一遍,然后記錄每一次調(diào)用的一些狀態(tài),這樣才能更好的理解 applyIncludes 方法的邏輯。

    好了,本節(jié)內(nèi)容先到這里,繼續(xù)往下分析。

    ?2.1.5.2 解析 <selectKey> 節(jié)點(diǎn)

    對于一些不支持自增主鍵的數(shù)據(jù)庫來說,我們在插入數(shù)據(jù)時,需要明確指定主鍵數(shù)據(jù)。以 Oracle 數(shù)據(jù)庫為例,Oracle 數(shù)據(jù)庫不支持自增主鍵,但它提供了自增序列工具。我們每次向數(shù)據(jù)庫中插入數(shù)據(jù)時,可以先通過自增序列獲取主鍵數(shù)據(jù),然后再進(jìn)行插入。這里涉及到兩次數(shù)據(jù)庫查詢操作,我們不能在一個 <select> 節(jié)點(diǎn)中同時定義兩個 select 語句,否者會導(dǎo)致 SQL 語句出錯。對于這個問題,MyBatis 的 <selectKey> 可以很好的解決。下面我們看一段配置:

    1 2 3 4 5 6 7 8 9 <insert id="saveAuthor"><selectKey keyProperty="id" resultType="int" order="BEFORE">select author_seq.nextval from dual</selectKey>insert into Author(id, name, password)values(#{id}, #{username}, #{password}) </insert>

    在上面的配置中,查詢語句會先于插入語句執(zhí)行,這樣我們就可以在插入時獲取到主鍵的值。關(guān)于 <selectKey> 的用法,這里不過多介紹了。下面我們來看一下 <selectKey> 節(jié)點(diǎn)的解析過程。

    1 2 3 4 5 6 7 8 9 10 11 private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {List<XNode> selectKeyNodes = context.evalNodes("selectKey");if (configuration.getDatabaseId() != null) {// 解析 <selectKey> 節(jié)點(diǎn),databaseId 不為空parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());}// 解析 <selectKey> 節(jié)點(diǎn),databaseId 為空parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);// 將 <selectKey> 節(jié)點(diǎn)從 dom 樹中移除removeSelectKeyNodes(selectKeyNodes); }

    從上面的代碼中可以看出,<selectKey> 節(jié)點(diǎn)在解析完成后,會被從 dom 樹中移除。這樣后續(xù)可以更專注的解析 <insert> 或 <update> 節(jié)點(diǎn)中的 SQL,無需再額外處理 <selectKey> 節(jié)點(diǎn)。繼續(xù)往下看。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass,LanguageDriver langDriver, String skRequiredDatabaseId) {for (XNode nodeToHandle : list) {// id = parentId + !selectKey,比如 saveUser!selectKeyString id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;// 獲取 <selectKey> 節(jié)點(diǎn)的 databaseId 屬性String databaseId = nodeToHandle.getStringAttribute("databaseId");// 匹配 databaseIdif (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {// 解析 <selectKey> 節(jié)點(diǎn)parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);}} }private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass,LanguageDriver langDriver, String databaseId) {// 獲取各種屬性String resultType = nodeToHandle.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));String keyProperty = nodeToHandle.getStringAttribute("keyProperty");String keyColumn = nodeToHandle.getStringAttribute("keyColumn");boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));// 設(shè)置默認(rèn)值boolean useCache = false;boolean resultOrdered = false;KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;Integer fetchSize = null;Integer timeout = null;boolean flushCache = false;String parameterMap = null;String resultMap = null;ResultSetType resultSetTypeEnum = null;// 創(chuàng)建 SqlSourceSqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);/** <selectKey> 節(jié)點(diǎn)中只能配置 SELECT 查詢語句,* 因此 sqlCommandType 為 SqlCommandType.SELECT*/SqlCommandType sqlCommandType = SqlCommandType.SELECT;/** 構(gòu)建 MappedStatement,并將 MappedStatement * 添加到 Configuration 的 mappedStatements map 中*/builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);// id = namespace + "." + idid = builderAssistant.applyCurrentNamespace(id, false);MappedStatement keyStatement = configuration.getMappedStatement(id, false);// 創(chuàng)建 SelectKeyGenerator,并添加到 keyGenerators map 中configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }

    上面的源碼比較長,但大部分代碼都是一些基礎(chǔ)代碼,不是很難理解。以上代碼比較重要的步驟如下:

  • 創(chuàng)建 SqlSource 實(shí)例
  • 構(gòu)建并緩存 MappedStatement 實(shí)例
  • 構(gòu)建并緩存 SelectKeyGenerator 實(shí)例
  • 在這三步中,第1步和第2步調(diào)用的是公共邏輯,其他地方也會調(diào)用,這兩步對應(yīng)的源碼后續(xù)會分兩節(jié)進(jìn)行講解。第3步則是創(chuàng)建一個 SelectKeyGenerator 實(shí)例,SelectKeyGenerator 創(chuàng)建的過程本身沒什么好說的,所以就不多說了。下面分析一下 SqlSource 和 MappedStatement 實(shí)例的創(chuàng)建過程。

    ?2.1.5.3 解析 SQL 語句

    前面分析了 <include> 和 <selectKey> 節(jié)點(diǎn)的解析過程,這兩個節(jié)點(diǎn)解析完成后,都會以不同的方式從 dom 樹中消失。所以目前的 SQL 語句節(jié)點(diǎn)由一些文本節(jié)點(diǎn)和普通節(jié)點(diǎn)組成,比如 <if>、<where> 等。那下面我們來看一下移除掉 <include> 和 <selectKey> 節(jié)點(diǎn)后的 SQL 語句節(jié)點(diǎn)是如何解析的。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // -☆- XMLLanguageDriver public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode(); }// -☆- XMLScriptBuilder public SqlSource parseScriptNode() {// 解析 SQL 語句節(jié)點(diǎn)MixedSqlNode rootSqlNode = parseDynamicTags(context);SqlSource sqlSource = null;// 根據(jù) isDynamic 狀態(tài)創(chuàng)建不同的 SqlSourceif (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource; }

    如上,SQL 語句的解析邏輯被封裝在了 XMLScriptBuilder 類的 parseScriptNode 方法中。該方法首先會調(diào)用 parseDynamicTags 解析 SQL 語句節(jié)點(diǎn),在解析過程中,會判斷節(jié)點(diǎn)是是否包含一些動態(tài)標(biāo)記,比如 ${} 占位符以及動態(tài) SQL 節(jié)點(diǎn)等。若包含動態(tài)標(biāo)記,則會將 isDynamic 設(shè)為 true。后續(xù)可根據(jù) isDynamic 創(chuàng)建不同的 SqlSource。下面,我們來看一下 parseDynamicTags 方法的邏輯。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 /** 該方法用于初始化 nodeHandlerMap 集合,該集合后面會用到 */ private void initNodeHandlerMap() {nodeHandlerMap.put("trim", new TrimHandler());nodeHandlerMap.put("where", new WhereHandler());nodeHandlerMap.put("set", new SetHandler());nodeHandlerMap.put("foreach", new ForEachHandler());nodeHandlerMap.put("if", new IfHandler());nodeHandlerMap.put("choose", new ChooseHandler());nodeHandlerMap.put("when", new IfHandler());nodeHandlerMap.put("otherwise", new OtherwiseHandler());nodeHandlerMap.put("bind", new BindHandler()); }protected MixedSqlNode parseDynamicTags(XNode node) {List<SqlNode> contents = new ArrayList<SqlNode>();NodeList children = node.getNode().getChildNodes();// 遍歷子節(jié)點(diǎn)for (int i = 0; i < children.getLength(); i++) {XNode child = node.newXNode(children.item(i));if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {// 獲取文本內(nèi)容String data = child.getStringBody("");TextSqlNode textSqlNode = new TextSqlNode(data);// 若文本中包含 ${} 占位符,也被認(rèn)為是動態(tài)節(jié)點(diǎn)if (textSqlNode.isDynamic()) {contents.add(textSqlNode);// 設(shè)置 isDynamic 為 trueisDynamic = true;} else {// 創(chuàng)建 StaticTextSqlNodecontents.add(new StaticTextSqlNode(data));}// child 節(jié)點(diǎn)是 ELEMENT_NODE 類型,比如 <if>、<where> 等} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {// 獲取節(jié)點(diǎn)名稱,比如 if、where、trim 等String nodeName = child.getNode().getNodeName();// 根據(jù)節(jié)點(diǎn)名稱獲取 NodeHandlerNodeHandler handler = nodeHandlerMap.get(nodeName);/** 如果 handler 為空,表明當(dāng)前節(jié)點(diǎn)對與 MyBatis 來說,是未知節(jié)點(diǎn)。* MyBatis 無法處理這種節(jié)點(diǎn),故拋出異常*/ if (handler == null) {throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");}// 處理 child 節(jié)點(diǎn),生成相應(yīng)的 SqlNodehandler.handleNode(child, contents);// 設(shè)置 isDynamic 為 trueisDynamic = true;}}return new MixedSqlNode(contents); }

    上面方法的邏輯我前面已經(jīng)說過,主要是用來判斷節(jié)點(diǎn)是否包含一些動態(tài)標(biāo)記,比如 ${} 占位符以及動態(tài) SQL 節(jié)點(diǎn)等。這里,不管是動態(tài) SQL 節(jié)點(diǎn)還是靜態(tài) SQL 節(jié)點(diǎn),我們都可以把它們看成是 SQL 片段,一個 SQL 語句由多個 SQL 片段組成。在解析過程中,這些 SQL 片段被存儲在 contents 集合中。最后,該集合會被傳給 MixedSqlNode 構(gòu)造方法,用于創(chuàng)建 MixedSqlNode 實(shí)例。從 MixedSqlNode 類名上可知,它會存儲多種類型的 SqlNode。除了上面代碼中已出現(xiàn)的幾種 SqlNode 實(shí)現(xiàn)類,還有一些 SqlNode 實(shí)現(xiàn)類未出現(xiàn)在上面的代碼中。但它們也參與了 SQL 語句節(jié)點(diǎn)的解析過程,這里我們來看一下這些幕后的 SqlNode 類。

    上面的 SqlNode 實(shí)現(xiàn)類用于處理不同的動態(tài) SQL 邏輯,這些 SqlNode 是如何生成的呢?答案是由各種 NodeHandler 生成。我們再回到上面的代碼中,可以看到這樣一句代碼:

    1 handler.handleNode(child, contents);

    該代碼用于處理動態(tài) SQL 節(jié)點(diǎn),并生成相應(yīng)的 SqlNode。下面來簡單分析一下 WhereHandler 的代碼。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** 定義在 XMLScriptBuilder 中 */ private class WhereHandler implements NodeHandler {public WhereHandler() {}@Overridepublic void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {// 調(diào)用 parseDynamicTags 解析 <where> 節(jié)點(diǎn)MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);// 創(chuàng)建 WhereSqlNodeWhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);// 添加到 targetContentstargetContents.add(where);} }

    如上,handleNode 方法內(nèi)部會再次調(diào)用 parseDynamicTags 解析 <where> 節(jié)點(diǎn)中的內(nèi)容,這樣又會生成一個 MixedSqlNode 對象。最終,整個 SQL 語句節(jié)點(diǎn)會生成一個具有樹狀結(jié)構(gòu)的 MixedSqlNode。如下圖:

    到此,SQL 語句的解析過程就分析完了?,F(xiàn)在,我們已經(jīng)將 XML 配置解析了 SqlSource,但這還沒有結(jié)束。SqlSource 中只能記錄 SQL 語句信息,除此之外,這里還有一些額外的信息需要記錄。因此,我們需要一個類能夠同時存儲 SqlSource 和其他的信息。這個類就是 MappedStatement。下面我們來看一下它的構(gòu)建過程。

    ?2.1.5.4 構(gòu)建 MappedStatement

    SQL 語句節(jié)點(diǎn)可以定義很多屬性,這些屬性和屬性值最終存儲在 MappedStatement 中。下面我們看一下 MappedStatement 的構(gòu)建過程是怎樣的。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType,Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache,boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty,String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {if (unresolvedCacheRef) {throw new IncompleteElementException("Cache-ref not yet resolved");}id = applyCurrentNamespace(id, false);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;// 創(chuàng)建建造器,設(shè)置各種屬性MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).resultSetType(resultSetType).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);// 獲取或創(chuàng)建 ParameterMapParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}// 構(gòu)建 MappedStatement,沒有什么復(fù)雜邏輯,不跟下去了MappedStatement statement = statementBuilder.build();// 添加 MappedStatement 到 configuration 的 mappedStatements 集合中configuration.addMappedStatement(statement);return statement; }

    上面就是 MappedStatement,沒什么復(fù)雜的地方,就不多說了。

    ?2.1.6 小節(jié)

    本章分析了映射文件的解析過程,總的來說,本章的內(nèi)容還是比較復(fù)雜的,邏輯太多。不過如果大家自己也能把映射文件的解析過程認(rèn)真分析一遍,會對 MyBatis 有更深入的理解。分析過程很累,但是在此過程中會收獲了很多東西,還是很開心的。好了,本章內(nèi)容先到這里。后面還有一些代碼需要分析,我們繼續(xù)往后看。

    ?2.2 Mapper 接口綁定過程分析

    映射文件解析完成后,并不意味著整個解析過程就結(jié)束了。此時還需要通過命名空間綁定 mapper 接口,這樣才能將映射文件中的 SQL 語句和 mapper 接口中的方法綁定在一起,后續(xù)即可通過調(diào)用 mapper 接口方法執(zhí)行與之對應(yīng)的 SQL 語句。下面我們來分析一下 mapper 接口的綁定過程。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // -☆- XMLMapperBuilder private void bindMapperForNamespace() {// 獲取映射文件的命名空間String namespace = builderAssistant.getCurrentNamespace();if (namespace != null) {Class<?> boundType = null;try {// 根據(jù)命名空間解析 mapper 類型boundType = Resources.classForName(namespace);} catch (ClassNotFoundException e) {}if (boundType != null) {// 檢測當(dāng)前 mapper 類是否被綁定過if (!configuration.hasMapper(boundType)) {configuration.addLoadedResource("namespace:" + namespace);// 綁定 mapper 類configuration.addMapper(boundType);}}} }// -☆- Configuration public <T> void addMapper(Class<T> type) {// 通過 MapperRegistry 綁定 mapper 類mapperRegistry.addMapper(type); }// -☆- MapperRegistry public <T> void addMapper(Class<T> type) {if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {/** 將 type 和 MapperProxyFactory 進(jìn)行綁定,* MapperProxyFactory 可為 mapper 接口生成代理類*/knownMappers.put(type, new MapperProxyFactory<T>(type));// 創(chuàng)建注解解析器。在 MyBatis 中,有 XML 和 注解兩種配置方式可選MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);// 解析注解中的信息parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}} }

    以上就是 Mapper 接口的綁定過程。這里簡單一下:

  • 獲取命名空間,并根據(jù)命名空間解析 mapper 類型
  • 將 type 和 MapperProxyFactory 實(shí)例存入 knownMappers 中
  • 解析注解中的信息
  • 以上步驟中,第3步的邏輯較多。如果大家看懂了映射文件的解析過程,那么注解的解析過程也就不難理解了,這里就不深入分析了。好了,Mapper 接口的綁定過程就先分析到這。

    ?2.3 處理未完成解析的節(jié)點(diǎn)

    在解析某些節(jié)點(diǎn)的過程中,如果這些節(jié)點(diǎn)引用了其他一些未被解析的配置,會導(dǎo)致當(dāng)前節(jié)點(diǎn)解析工作無法進(jìn)行下去。對于這種情況,MyBatis 的做法是拋出 IncompleteElementException 異常。外部邏輯會捕捉這個異常,并將節(jié)點(diǎn)對應(yīng)的解析器放入 incomplet* 集合中。這個我在分析映射文件解析的過程中進(jìn)行過相應(yīng)注釋,不知道大家有沒有注意到。沒注意到也沒關(guān)系,待會我會舉例說明。下面我們來看一下 MyBatis 是如何處理未完成解析的節(jié)點(diǎn)。

    1 2 3 4 5 6 7 8 9 10 11 12 // -☆- XMLMapperBuilder public void parse() {// 省略部分代碼// 解析 mapper 節(jié)點(diǎn)configurationElement(parser.evalNode("/mapper"));// 處理未完成解析的節(jié)點(diǎn)parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements(); }

    如上,parse 方法是映射文件的解析入口。在本章的開始,我貼過這個源碼。從上面的源碼中可以知道有三種節(jié)點(diǎn)在解析過程中可能會出現(xiàn)不能完成解析的情況。由于上面三個以 parsePending 開頭的方法邏輯一致,所以下面我只會分析其中一個方法的源碼。簡單起見,這里選擇分析 parsePendingCacheRefs 的源碼。下面看一下如何配置映射文件會導(dǎo)致 <cache-ref> 節(jié)點(diǎn)無法完成解析。

    1 2 3 4 5 6 7 8 9 10 <!-- 映射文件1 --> <mapper namespace="xyz.coolblog.dao.Mapper1"><!-- 引用映射文件2中配置的緩存 --><cache-ref namespace="xyz.coolblog.dao.Mapper2"/> </mapper><!-- 映射文件2 --> <mapper namespace="xyz.coolblog.dao.Mapper2"><cache/> </mapper>

    如上,假設(shè) MyBatis 先解析映射文件1,然后再解析映射文件2。按照這樣的解析順序,映射文件1中的 <cache-ref> 節(jié)點(diǎn)就無法完成解析,因?yàn)樗玫木彺孢€未被解析。當(dāng)映射文件2解析完成后,MyBatis 會調(diào)用 parsePendingCacheRefs 方法處理在此之前未完成解析的 <cache-ref> 節(jié)點(diǎn)。具體的邏輯如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private void parsePendingCacheRefs() {// 獲取 CacheRefResolver 列表Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();synchronized (incompleteCacheRefs) {Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();// 通過迭代器遍歷列表while (iter.hasNext()) {try {/** 嘗試解析 <cache-ref> 節(jié)點(diǎn),若解析失敗,則拋出 IncompleteElementException,* 此時下面的刪除操作不會被執(zhí)行*/iter.next().resolveCacheRef();/** 移除 CacheRefResolver 對象。如果代碼能執(zhí)行到此處,* 表明已成功解析了 <cache-ref> 節(jié)點(diǎn)*/iter.remove();} catch (IncompleteElementException e) {/** 如果再次發(fā)生 IncompleteElementException 異常,表明當(dāng)前映射文件中并沒有 * <cache-ref> 所引用的緩存。有可能所引用的緩存在后面的映射文件中,所以這里* 不能將解析失敗的 CacheRefResolver 從集合中刪除*/}}} }

    上面代碼不是很長,我也做了比較多的注釋,應(yīng)該不難理解。好了,關(guān)于未完成解析節(jié)點(diǎn)的解析過程就分析到這。

    ?3.總結(jié)

    本篇文章對映射文件的解析過程進(jìn)行了較為詳細(xì)的分析,全文篇幅比較大,寫的也比較辛苦。本篇文章耗時7天完成,在這7天中,基本上一有空閑時間,就會用來寫作。雖然很累,但是收獲也很多。我目前正在努力的構(gòu)建自己的知識體系,我覺得對于常用的技術(shù),還是應(yīng)該花一些時間和精力,去弄懂它的原理。這樣以后才能走的更遠(yuǎn),才能成為你想成為的樣子。

    好了,其他的就不多說了,本篇文章就到這吧。謝謝大家的閱讀。

    ?參考

    • 《MyBatis 技術(shù)內(nèi)幕》- 徐郡明
    • MyBatis 官方文檔

    ?附錄:MyBatis 源碼分析系列文章列表

    更新時間標(biāo)題
    2018-07-16MyBatis 源碼分析系列文章導(dǎo)讀
    2018-07-20MyBatis 源碼分析 - 配置文件解析過程
    2018-07-30MyBatis 源碼分析 - 映射文件解析過程
    2018-08-17MyBatis 源碼分析 - SQL 的執(zhí)行過程
    2018-08-19MyBatis 源碼分析 - 內(nèi)置數(shù)據(jù)源
    2018-08-25MyBatis 源碼分析 - 緩存原理
    2018-08-26MyBatis 源碼分析 - 插件機(jī)制
    • 本文鏈接:?https://www.tianxiaobo.com/2018/07/30/MyBatis-源碼分析-映射文件解析過程/

    http://www.tianxiaobo.com/2018/07/30/MyBatis-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E6%98%A0%E5%B0%84%E6%96%87%E4%BB%B6%E8%A7%A3%E6%9E%90%E8%BF%87%E7%A8%8B/?

    總結(jié)

    以上是生活随笔為你收集整理的MyBatis 源码分析 - 映射文件解析过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。