Mybatis源碼閱讀(一) 配置文件的加載及查詢過程

目標(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ū)別,這張圖畫起來也有很多形式。我們先從總體上建立一個印象。每一層的主要對象和主要的功能我們也給大家分析一下。

image.png

接口層

首先接口層是我們打交道最多的。核心對象是 SqlSession,它是上層應(yīng)用和 MyBatis打交道的橋梁,SqlSession 上定義了非常多的對數(shù)據(jù)庫的操作方法。接口層在接收到調(diào)用請求的時候,會調(diào)用核心處理層的相應(yīng)模塊來完成具體的數(shù)據(jù)庫操作。

核心處理層

接下來是核心處理層。既然叫核心處理層,也就是跟數(shù)據(jù)庫操作相關(guān)的動作都是在這一層完成的。核心處理層主要做了這幾件事:

  1. 把接口中傳入的參數(shù)解析并且映射成 JDBC 類型;
  2. 解析 xml 文件中的 SQL 語句,包括插入?yún)?shù),和動態(tài) SQL 的生成;
  3. 執(zhí)行 SQL 語句;
  4. 處理結(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)簽

image.png

根據(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è)置時,不妨看看這里,我這里從別的文章中截圖簡要說明一下上面屬性的意思,到目前為止只是解析配置,至于怎么用這些配置,別著急,后面會說。



image.png



大體有個印象,回查。

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ì),就是他啦


image.png

然后我們根據(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個步驟:

  1. 創(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);
    }
image.png

一看到這個Base開頭的抽象類,就知道他肯定存儲著公共變量和公共方法,提高了代碼的復(fù)用性,在策略模式和模板方法模式中很常用。在這里就用到了模板方法模式

模板方法模式,定義了一個算法骨架,并允許子類為一個或多個步驟提供實(shí)現(xiàn)。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法的某些步驟。

  1. 緩存裝飾
    如果cacheEnable=true,會用裝飾器模式對executor進(jìn)行裝飾。
   // 二級緩存開關(guān),settings 中的 cacheEnabled 默認(rèn)是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
  1. 插件代理
// 植入插件的邏輯,至此,四大對象已經(jīng)全部攔截完畢
    executor = (Executor) interceptorChain.pluginAll(executor);
  1. 返回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。


image.png

四、執(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。


image.png

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);
    }
  }

  1. 創(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。

  2. 創(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;
  }
  1. 執(zhí)行的 StatementHandler 的 query()方法
    RoutingStatementHandler 的 query()方法。
    delegate 委派,最終執(zhí)行 PreparedStatementHandler 的 query()方法。

  2. 執(zhí)行 PreparedStatement 的 execute()方法
    后面就是 JDBC 包中的 PreparedStatement 的執(zhí)行了。

  3. ResultSetHandler 處理結(jié)果集
    return resultSetHandler.handleResultSets(ps);
    ResultSetHandler 只有一個實(shí)現(xiàn)類:DefaultResultSetHandler。也就是執(zhí)DefaultResultSetHandler 的 handleResultSets ()方法。
    首先我們會先拿到第一個結(jié)果集,如果沒有配置一個查詢返回多個結(jié)果集的情況,一般只有一個結(jié)果集。如果下面的這個 while 循環(huán)我們也不用,就是執(zhí)行一次。然后會調(diào)用 handleResultSet()方法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內(nèi)容