Mybatis 源碼解析—— 加載 mybatis-config.xml

本文詳細的分析了 Mybatis 配置文件 mybatis-config.xml 的解析過程。其中包括各類屬性的加載,佔位符替換等重要功能。跟着本文一塊兒分析、理解整個過程。php

測試程序

建立測試類java

public class BaseFlowTest {
  @Test
  public void baseTest() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    try (SqlSession session = sqlSessionFactory.openSession()) {
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(1);
    }
  }
}
複製代碼

配置文件以下:算法

<?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="classpath:jdbc.properties"/>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/BlogMapper.xml"/>
  </mappers>
</configuration>
複製代碼

解析步驟

因爲本模塊主要研究 parsing 模塊的做用——負責配置文件的解析,因此忽略資源文件流的獲取,定位到 SqlSessionFactory 的構建sql

SqlSessionFactory 構建

進入到 build(Inputstream) 方法,調用了 build(Inputstream,String,Properties)方法。數據庫

build

該方法的功能主要分爲兩步:express

  • 構造 XMLConfigBuilder
  • 調用 XMLConfigBuilderparse() 方法解析配置文件

構造 XMLConfigBuilder

XMLConfigBuilder

構造 XPathParser,且傳入的 entityResolverXMLMapperEntityResolverapache

XPathParser

XPathParser

XPathParser 是 Mybatis 對 XPath 解析器的擴展,用於解析 mybatis-config.xml*Mapper.xml 等 XML 配置文件。session

該類包括五個屬性:mybatis

  • Document document:XML 文檔 DOM 對象
  • boolean validation:是否對文檔基於 DTD 或 XSD 校驗
  • EntityResolver entityResolver:XML 實體解析器
  • Properties variablesmybatis-config.xmlproperties 標籤下獲取的鍵值對集合(包括引入的配置文件)
  • XPath xpathXPath 解析器

圖中的 XPathParser 構造方法即爲 XMLConfigBuilder 調用的構造方法,它調用了該類的一個通用賦值方法,而後調用 createDocument 方法根據 XML 文件的輸入流建立一個 DOM 對象。app

commonConstructor

簡單的賦值操做。

createDocument

createDocument 方法的過程也比較簡單,分爲三步:

  • 根據設置建立 DocumentBuilderFactory 對象
  • DocumentBuilderFactory 中建立 DocumentBuilder 對象並設置屬性
  • 調用 XPath 解析 XML 文件

初始化 configuration

經過 XPathParser 獲取了 XML DOM 對象以後,調用了該類的通用構造方法

XMLConfigBuilder

該方法調用了父類的構造方法,但主要仍是在於初始化了 Configuration,主要是設置了 Mybatis 中默認的 TypeAliasTypeHandler,初始化了用來存儲不一樣配置的各類容器。

parse

初始化配置以後,調用 parse 方法。

parse

該方法首先會檢查配置文件是否已經被解析過。若是沒有解析過,進行如下兩個步驟:

  • 使用 XPathParser 獲取 XML configuration 節點內容
  • 解析 configuration 下的所有配置
evalNode

evalNode

evalNode 方法可以根據 XPath 表達式獲取知足表達式的節點內容,最後會將 Node 類型的內容封裝成 XNode 類型的對象,便於替換動態值。

這個方法只會獲取特定的一個節點的內容,對應的還有 evalNodes 方法,能夠獲取知足表達式的全部節點內容。

parseConfiguration 解析 mybatis-config.xml

這個方法能夠說是解析 mybatis-config.xml 中最核心的方法了,它彙總瞭解析 XML 中各類自定義值的方法。

parseConfiguration

下面會分析這個方法調用的一些關鍵方法

propertiesElement

propertiesElement

propertiesElement 方法用來獲取 mybatis-config.xml<properties> 標籤中配置的鍵值對,包括引入的配置文件中的鍵值對。

方法步驟以下:

  • 獲取 properties 子節點 property 下全部鍵值對
  • 獲取 properties 標籤中的 url 或者 resource 屬性指定資源中的鍵值對(兩個屬性只能存在一個)
  • 將獲取到的鍵值對集合放入 XMLConfigBuilder 類的 configurationparser 變量中。
settingsAsProperties

settingsAsProperties

settingsAsProperties 會解析 settings 標籤下一些配置的值,例如經常使用的 mapUnderscoreToCamelCaseuseGeneratedKeys 等配置。

方法步驟:

  • 獲取 settings 標籤下全部的鍵值對
  • 獲取 Configuration 類對應的 MetaClass (Mybatis 封裝的反射工具類,包括了該類的各類元數據信息)
  • 經過 metaConfig 判斷 Mybatis 是否支持 setting 標籤中的配置的 key,不支持直接拋出異常
  • 返回 settings 下的鍵值對集合
typeAliasesElement

typeAliasesElement

typeAliasesElement 方法用來獲取 mybatis-config.xmltypeAliases 配置的 typeAlias

方法步驟爲:

  • 獲取 typeAliases 的子標籤
  • 解析子標籤
    • 解析 package 標籤
    • 解析 typeAlias 標籤
  • 爲類註冊別名

typeAliases 標籤下有兩種子標籤:typeAliaspackage

實際上, package 標籤的解析過程會包括 typeAlias 中屬性的解析過程,因此我直接分析 package 標籤的 typeAlias 獲取過程便可。

registerAliases

registerAliases 方法的步驟以下:

  • 調用 Mybatis io 包下的 ResolverUtil 類的 find 方法,藉助 VFS 找到指定包下的 class 文件
  • 遍歷獲取到的每一個類,過濾內部類、接口以及匿名類,調用別名註冊方法 registerAlias

registerAlias

該方法會將自定義的別名設置爲小寫,並判斷別名是否有對應值,若是沒有,則註冊成功。

pluginElement

pluginElement

pluginElement 方法會加載自定義的插件。

該方法步驟以下:

  • 遍歷 pluginsplugin 節點
  • 獲取 plugin 節點 interceptor 屬性值,並根據屬性值加載對應類。(此時別名已經加載完,因此該方法會先判斷屬性值是否爲別名。若不是,則用 Resources 類加載對應的類文件。
  • interceptor 加載設置的屬性,並將 interceptor 加入 configuration 中。
objectFactoryElement

objectFactoryElement

這個配置實際用的很少,主要是用來覆蓋默認對象工廠的對象實例化行爲,能夠建立符合本身需求的對象。

objectFactoryElement 方法的過程和 pluginElement 過程徹底一致。只不過獲取的屬性爲 type

objectWrapperFactoryElement

objectWrapperFactoryElement

objectWrapperFactoryElement 方法與 objectFactoryElement 一致,只不過建立的對象變成了 ObjectWrapper,該類對對象屬性操做提供了包裝好的方法。

ObjectWrapper

reflectorFactoryElement

reflectorFactoryElement

reflectorFactoryElement 方法同 objectWrapperFactoryElement 同樣,會建立一個關於對象元信息的類 Reflector,該類也是封裝了類元信息的一些反射方法。

Reflector

settingsElement

settingsElementsettings 下的配置加載到 configuration

由於其中有不少與 SQL 相關的配置項,因此須要加載 SQL 鏈接信息以前加載到 configuration 中。

environmentsElement

environmentsElement

environmentsElement 方法會解析 mybatis-config.xml 中關於數據庫鏈接的配置。

environments 下能夠存在多個 environment 標籤,可是 Mybatis 只會加載 id 等於 environmentsdefault 屬性值的 environment

主要的步驟爲:

  • 獲取默認環境 id
  • 遍歷 environment,只有 id 與默認 id 相等,才進入後續流程
  • 解析 transactionManager 標籤獲取指定事務類型的工廠,解析 dataSource 標籤獲取指定數據源類型的工廠
  • 根據上述工廠構建 Environment 放入 configuration
databaseIdProviderElement

databaseIdProviderElement

databaseIdProviderElement 方法用來解析多數據源配置。

該方法步驟爲:

  • 獲取 databaseIdProvider 標籤 type 屬性值對應的 DatabaseIdProvider
  • 獲取 configuration 中的數據庫鏈接信息
  • 鏈接數據庫,獲取數據庫的產品名,與 databaseIdProvider 標籤下的子標籤的 name 屬性匹配
  • 將匹配上的 name 對應的 value 設置爲 databaseId 並放入 configuration
typeHandlerElement

typeHandlerElement

typeHandlerElement 方法用來註冊自定義 typeHandler

整個方法流程與 typeAliases 相似,只是最後存放配置的容器不一樣,此處再也不說明。

mapperElement

mapperElement

mapperElement 方法用於解析 *Mapper.xml 配置文件或 *Mapper 接口。

該方法主要步驟爲:

  • 遍歷 mappers 下每一個節點
  • 根據子標籤類型來解析
    • 若是子標籤爲 package, 則掃描包下的全部接口
    • 若是子標籤爲 mapper,獲取 resourceurlclass 中不爲空的屬性值,而後根據具體屬性進行對應的解析
  • 使用 MapperBuilderparse 方法解析對應的 XML 或者接口類

Mapper 映射配置會在後續詳解。

至此,mybatis-config.xml 中的配置已所有加載完成

補充

佔位符加載

在解析 environments的模塊,mybatis-config.xml 中的配置是這樣,用到了佔位符方便動態替換數據源的鏈接信息。

<dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</dataSource>
複製代碼

在解析佔位符以前,存放鍵值對的文件已經被加載過,鍵值對存放在 XMLConfigBuildervariables 屬性和 XPathParservariables 屬性中。

接下來分析這些佔位符是什麼時候以及怎樣被替換的。

environmentsElement 開始

environmentsElement

能夠看到,該方法在獲取數據源工廠時解析了 dataSource 節點,以後調用 dataSourceElement 方法處理該節點。

DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
複製代碼

進入 DataSourceElement 方法。

DataSourceElement

這裏調用了 getChildrenAsProperties 方法來解析 datasource 標籤下的子標籤

Properties props = context.getChildrenAsProperties();
複製代碼

getChildrenAsProperties

這裏經過 getChildren 方法獲取了 datasource 下的全部子元素(此處,佔位符已經被替換;和 JavaScript 的 DOM 同樣,文本節點也會被獲取)。跟蹤進 getChildren 方法。

getChildren

getChildren 方法調用了 getChildNodes 獲得了全部子元素。以後遍歷該節點的子元素,若是子元素節點類型爲 ELEMENT_NODE(元素節點),則構造 XNode 類型的節點,加入返回給 getChildrenAsProperties 方法的集合中。

此處特地構造 XNode 類型的節點而不是直接返回 Node 類型的節點 ,是由於在構造 XNode 節點的過程當中,作了動態值的替換。能夠看到,在調用 XNode 構造方法時,將存放資源文件中鍵值對的變量 variables 做爲參數傳遞給了 XNode

XNode

前文中,當獲得了一個元素節點時,例如 <property name="driver" value="${driver}"/>,會調用該構造函數,在該構造函數中,會解析節點的屬性和節點的內容體。佔位符佔用的便是節點的一個屬性。

因此咱們進入 parseAttributes 方法。

parseAttributes

該方法內,會遍歷節點的全部屬性,調用 PropertyParserparse 方法替換佔位符。

終於進入到替換佔位符的核心方法了。

parse

該方法裏,構造了變量符號處理器 VariableTokenHandler ,並傳給給通用符號解析器 GenericTokenParser,這裏直接將變量的開始符號設置爲 ${,結束符號爲 },與咱們的佔位符 ${driver} 一致。

以後調用 parse 方法替換佔位符。

public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // 找佔位符開始標記
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    // 用來記錄解析後的字符串
    final StringBuilder builder = new StringBuilder();
    // 記錄佔位符的字面值。假設動態值爲 ${val},則 expression = val
    StringBuilder expression = null;
    while (start > -1) {
      // 若是 openToken 前面有轉義符
      if (start > 0 && src[start - 1] == '\\') {
        // 不解析該佔位符字面值,直接獲取去掉轉義符後的值
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      }
      // 若是 openToken 前面無轉義符
      else {
        if (expression == null) {
          expression = new StringBuilder();
        } // 若是以前找到過佔位符字面值,此次將它清空
        else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          // 找到佔位符結束標記
          // 若是這個結束標記前有轉義符,則結果中直接拼上去掉轉義符後的字符串,從新查找 佔位符結束標記
          if (end > offset && src[end - 1] == '\\') {
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          }
          // 找到的佔位符結束標記前無轉義符,則將 openToken 與 closeTokenlse 之間的字面值賦給 express,等待後續解析
          expression.append(src, offset, end - offset);
          break;
        }

        // 若是沒有找到與 openToken 對應的 closeToken,則直接將所有字符串做爲結果字符串返回
        if (end == -1) {
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 使用特定的 TokenHandler 獲取佔位符字面值對應的值
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      // 當 offset 後沒有 openToken 時,跳出 while 循環
      start = text.indexOf(openToken, offset);
    }
    // 拼接 closeToken 以後的部分
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
複製代碼

這個方法很長,可是算法都很容易理解。並且 Mybatis 在源碼的 test 包下提供了不少測試類,其中就包括 org.apache.ibatis.parsing.GenericTokenParserTest。運行裏面的單元測試來理解這一段方法很容易。

在解釋方法流程以前,先詳細說明該方法中的幾個變量:

  • offset:用來標記文本中已被解析到的位置
  • builder:記錄已被解析的字符串
  • express:記錄佔位符中的變量

另外,若是佔位符的開始符號與結束符號以前有轉義符,那麼該符號不會被識別成佔位符。

用文字簡單解釋一下流程:

  1. 首先獲取文本中佔位符開始符號 ${ 出現的位置 start

  2. 若是有獲取到,判斷符號前一位是否是轉義符 \

    2.1 若是是,則將 offset${ 的字符串都拼接到已處理字符串 builder 中,且將 offset 移動到 start +openToken.length() 位置。進入第 7 步。

    2.2 若是不是,轉義符,說明此處爲佔位符的開始。進入第 3 步。

  3. 尋找結束符號 } 出現的位置 end

    3.1 若是找到,繼續第 4 步

    3.2 找不到,進入第 5 步

  4. 判斷符號前一位是否是轉義符 \

    4.1 若是是,將 offsetend 的值都放入 express 中,標記爲佔位符中的變量,offset 移動到 end + closeToken.length() 處。進入第 3 步,繼續尋找結束符。

    4.2 若是不是,則將 startend 之間的做爲佔位符變量傳入 express 中。進入第 6 步。

  5. 找不到結束符 },則將未解析部分所有加入 builder 中,將 offset 設置爲 src.length,即標記所有文本都被解析,進入第 9 步。

  6. 調用 VariableTokenHandlerhandleToken 方法獲取佔位符變量對應的值,未獲取到就返回佔位符原來的值。將該值拼接到 builder 中,將 offset 也移動至 end + closeToken.length() 位置。

  7. 調用 start = text.indexOf(openToken, offset) 從新尋找佔位符開始位置。

  8. 拼接最後一個佔位符結束標記 } 以後的字符串。

  9. 返回已解析字符串 builder.toString()

流程中調用的 handleToken 方法很簡單。相似與在 HashMap 中找一個 key 對應的值。 handleToken 中會有變量存在默認值的狀況(在實際開發中,基本上不會開啓變量默認值功能)。

至此,佔位符替換就完成了。

總結

這篇文章分析了加載 mybatis-config.xml 的過程,涉及到的方法不少,可是方法都不復雜,並且對於一些邏輯稍微複雜的方法,Mybatis 都提供了對應的測試類,使咱們更容易理解。

在加載過程當中,只詳細分析了 parsing 包下的方法,其它模塊的做用後續會分析。

相關文章
相關標籤/搜索