本文詳細的分析了 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
進入到 build(Inputstream)
方法,調用了 build(Inputstream,String,Properties)
方法。數據庫
該方法的功能主要分爲兩步:express
XMLConfigBuilder
XMLConfigBuilder
的 parse()
方法解析配置文件XMLConfigBuilder
構造 XPathParser
,且傳入的 entityResolver
爲 XMLMapperEntityResolver
。apache
XPathParser
XPathParser
是 Mybatis 對 XPath
解析器的擴展,用於解析 mybatis-config.xml
和 *Mapper.xml
等 XML 配置文件。session
該類包括五個屬性:mybatis
Document document
:XML 文檔 DOM 對象boolean validation
:是否對文檔基於 DTD 或 XSD 校驗EntityResolver entityResolver
:XML 實體解析器Properties variables
:mybatis-config.xml
中 properties
標籤下獲取的鍵值對集合(包括引入的配置文件)XPath xpath
:XPath
解析器圖中的 XPathParser
構造方法即爲 XMLConfigBuilder
調用的構造方法,它調用了該類的一個通用賦值方法,而後調用 createDocument
方法根據 XML 文件的輸入流建立一個 DOM 對象。app
簡單的賦值操做。
createDocument
方法的過程也比較簡單,分爲三步:
DocumentBuilderFactory
對象DocumentBuilderFactory
中建立 DocumentBuilder
對象並設置屬性XPath
解析 XML 文件configuration
經過 XPathParser
獲取了 XML DOM 對象以後,調用了該類的通用構造方法
該方法調用了父類的構造方法,但主要仍是在於初始化了 Configuration
,主要是設置了 Mybatis 中默認的 TypeAlias
和 TypeHandler
,初始化了用來存儲不一樣配置的各類容器。
parse
初始化配置以後,調用 parse
方法。
該方法首先會檢查配置文件是否已經被解析過。若是沒有解析過,進行如下兩個步驟:
XPathParser
獲取 XML configuration
節點內容configuration
下的所有配置evalNode
evalNode
方法可以根據 XPath
表達式獲取知足表達式的節點內容,最後會將 Node
類型的內容封裝成 XNode
類型的對象,便於替換動態值。
這個方法只會獲取特定的一個節點的內容,對應的還有 evalNodes
方法,能夠獲取知足表達式的全部節點內容。
parseConfiguration
解析 mybatis-config.xml
這個方法能夠說是解析 mybatis-config.xml
中最核心的方法了,它彙總瞭解析 XML 中各類自定義值的方法。
下面會分析這個方法調用的一些關鍵方法
propertiesElement
propertiesElement
方法用來獲取 mybatis-config.xml
中 <properties>
標籤中配置的鍵值對,包括引入的配置文件中的鍵值對。
方法步驟以下:
properties
子節點 property
下全部鍵值對properties
標籤中的 url
或者 resource
屬性指定資源中的鍵值對(兩個屬性只能存在一個)XMLConfigBuilder
類的 configuration
和 parser
變量中。settingsAsProperties
settingsAsProperties
會解析 settings
標籤下一些配置的值,例如經常使用的 mapUnderscoreToCamelCase
、useGeneratedKeys
等配置。
方法步驟:
settings
標籤下全部的鍵值對Configuration
類對應的 MetaClass
(Mybatis 封裝的反射工具類,包括了該類的各類元數據信息)metaConfig
判斷 Mybatis 是否支持 setting
標籤中的配置的 key,不支持直接拋出異常settings
下的鍵值對集合typeAliasesElement
typeAliasesElement
方法用來獲取 mybatis-config.xml
中 typeAliases
配置的 typeAlias
。
方法步驟爲:
typeAliases
的子標籤package
標籤typeAlias
標籤typeAliases
標籤下有兩種子標籤:typeAlias
和 package
。
實際上, package
標籤的解析過程會包括 typeAlias
中屬性的解析過程,因此我直接分析 package
標籤的 typeAlias
獲取過程便可。
registerAliases
方法的步驟以下:
io
包下的 ResolverUtil
類的 find
方法,藉助 VFS
找到指定包下的 class 文件registerAlias
該方法會將自定義的別名設置爲小寫,並判斷別名是否有對應值,若是沒有,則註冊成功。
pluginElement
pluginElement
方法會加載自定義的插件。
該方法步驟以下:
plugins
下 plugin
節點plugin
節點 interceptor
屬性值,並根據屬性值加載對應類。(此時別名已經加載完,因此該方法會先判斷屬性值是否爲別名。若不是,則用 Resources
類加載對應的類文件。interceptor
加載設置的屬性,並將 interceptor
加入 configuration
中。objectFactoryElement
這個配置實際用的很少,主要是用來覆蓋默認對象工廠的對象實例化行爲,能夠建立符合本身需求的對象。
objectFactoryElement
方法的過程和 pluginElement
過程徹底一致。只不過獲取的屬性爲 type
。
objectWrapperFactoryElement
objectWrapperFactoryElement
方法與 objectFactoryElement
一致,只不過建立的對象變成了 ObjectWrapper
,該類對對象屬性操做提供了包裝好的方法。
reflectorFactoryElement
reflectorFactoryElement
方法同 objectWrapperFactoryElement
同樣,會建立一個關於對象元信息的類 Reflector
,該類也是封裝了類元信息的一些反射方法。
settingsElement
settingsElement
將 settings
下的配置加載到 configuration
。
由於其中有不少與 SQL 相關的配置項,因此須要加載 SQL 鏈接信息以前加載到 configuration
中。
environmentsElement
environmentsElement
方法會解析 mybatis-config.xml
中關於數據庫鏈接的配置。
environments
下能夠存在多個 environment
標籤,可是 Mybatis 只會加載 id 等於 environments
的 default
屬性值的 environment
。
主要的步驟爲:
environment
,只有 id 與默認 id 相等,才進入後續流程transactionManager
標籤獲取指定事務類型的工廠,解析 dataSource
標籤獲取指定數據源類型的工廠Environment
放入 configuration
databaseIdProviderElement
databaseIdProviderElement
方法用來解析多數據源配置。
該方法步驟爲:
databaseIdProvider
標籤 type
屬性值對應的 DatabaseIdProvider
configuration
中的數據庫鏈接信息databaseIdProvider
標籤下的子標籤的 name
屬性匹配name
對應的 value
設置爲 databaseId
並放入 configuration
中typeHandlerElement
typeHandlerElement
方法用來註冊自定義 typeHandler
。
整個方法流程與 typeAliases
相似,只是最後存放配置的容器不一樣,此處再也不說明。
mapperElement
mapperElement
方法用於解析 *Mapper.xml
配置文件或 *Mapper
接口。
該方法主要步驟爲:
mappers
下每一個節點package
, 則掃描包下的全部接口mapper
,獲取 resource
、url
和 class
中不爲空的屬性值,而後根據具體屬性進行對應的解析MapperBuilder
的 parse
方法解析對應的 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>
複製代碼
在解析佔位符以前,存放鍵值對的文件已經被加載過,鍵值對存放在 XMLConfigBuilder
的 variables
屬性和 XPathParser
的 variables
屬性中。
接下來分析這些佔位符是什麼時候以及怎樣被替換的。
從 environmentsElement
開始
能夠看到,該方法在獲取數據源工廠時解析了 dataSource
節點,以後調用 dataSourceElement
方法處理該節點。
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
複製代碼
進入 DataSourceElement
方法。
這裏調用了 getChildrenAsProperties
方法來解析 datasource
標籤下的子標籤
Properties props = context.getChildrenAsProperties();
複製代碼
這裏經過 getChildren
方法獲取了 datasource
下的全部子元素(此處,佔位符已經被替換;和 JavaScript
的 DOM 同樣,文本節點也會被獲取)。跟蹤進 getChildren
方法。
getChildren
方法調用了 getChildNodes
獲得了全部子元素。以後遍歷該節點的子元素,若是子元素節點類型爲 ELEMENT_NODE
(元素節點),則構造 XNode
類型的節點,加入返回給 getChildrenAsProperties
方法的集合中。
此處特地構造 XNode
類型的節點而不是直接返回 Node
類型的節點 ,是由於在構造 XNode
節點的過程當中,作了動態值的替換。能夠看到,在調用 XNode
構造方法時,將存放資源文件中鍵值對的變量 variables
做爲參數傳遞給了 XNode
。
前文中,當獲得了一個元素節點時,例如 <property name="driver" value="${driver}"/>
,會調用該構造函數,在該構造函數中,會解析節點的屬性和節點的內容體。佔位符佔用的便是節點的一個屬性。
因此咱們進入 parseAttributes
方法。
該方法內,會遍歷節點的全部屬性,調用 PropertyParser
的 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
:記錄佔位符中的變量另外,若是佔位符的開始符號與結束符號以前有轉義符,那麼該符號不會被識別成佔位符。
用文字簡單解釋一下流程:
首先獲取文本中佔位符開始符號 ${
出現的位置 start
。
若是有獲取到,判斷符號前一位是否是轉義符 \
2.1 若是是,則將 offset
到 ${
的字符串都拼接到已處理字符串 builder
中,且將 offset
移動到 start +openToken.length()
位置。進入第 7 步。
2.2 若是不是,轉義符,說明此處爲佔位符的開始。進入第 3 步。
尋找結束符號 }
出現的位置 end
3.1 若是找到,繼續第 4 步
3.2 找不到,進入第 5 步
判斷符號前一位是否是轉義符 \
4.1 若是是,將 offset
到 end
的值都放入 express
中,標記爲佔位符中的變量,offset
移動到 end + closeToken.length()
處。進入第 3 步,繼續尋找結束符。
4.2 若是不是,則將 start
與 end
之間的做爲佔位符變量傳入 express
中。進入第 6 步。
找不到結束符 }
,則將未解析部分所有加入 builder
中,將 offset
設置爲 src.length
,即標記所有文本都被解析,進入第 9 步。
調用 VariableTokenHandler
的 handleToken
方法獲取佔位符變量對應的值,未獲取到就返回佔位符原來的值。將該值拼接到 builder
中,將 offset
也移動至 end + closeToken.length()
位置。
調用 start = text.indexOf(openToken, offset)
從新尋找佔位符開始位置。
拼接最後一個佔位符結束標記 }
以後的字符串。
返回已解析字符串 builder.toString()
。
流程中調用的 handleToken
方法很簡單。相似與在 HashMap
中找一個 key
對應的值。 handleToken
中會有變量存在默認值的狀況(在實際開發中,基本上不會開啓變量默認值功能)。
至此,佔位符替換就完成了。
這篇文章分析了加載 mybatis-config.xml
的過程,涉及到的方法不少,可是方法都不復雜,並且對於一些邏輯稍微複雜的方法,Mybatis 都提供了對應的測試類,使咱們更容易理解。
在加載過程當中,只詳細分析了 parsing
包下的方法,其它模塊的做用後續會分析。