參考 知識星球 中 芋道源碼 星球的源碼解析,一個活躍度非常高的 Java 技術社群,感興趣的小伙伴可以加入 芋道源碼 星球,一起學習😄
該系列文檔是本人在學習 Mybatis 的源碼過程中總結下來的,可能對讀者不太友好,請結合我的源碼注釋(Mybatis源碼分析 GitHub 地址、Mybatis-Spring 源碼分析 GitHub 地址、Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
該系列其他文檔請查看:《精盡 MyBatis 源碼分析 - 文章導讀》
MyBatis的初始化
在MyBatis初始化過程中,大致會有以下幾個步驟:
-
創建
Configuration
全局配置對象,會往TypeAliasRegistry
別名注冊中心添加Mybatis需要用到的相關類,並設置默認的語言驅動類為XMLLanguageDriver
-
加載
mybatis-config.xml
配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息會形成相應的對象並保存到Configuration全局配置對象中 -
構建
DefaultSqlSessionFactory
對象,通過它可以創建DefaultSqlSession
對象,MyBatis中SqlSession
的默認實現類
因為整個初始化過程涉及到的代碼比較多,所以拆分成了四個模塊依次對MyBatis的初始化進行分析:
- 《MyBatis初始化(一)之加載mybatis-config.xml》
- 《MyBatis初始化(二)之加載Mapper接口與XML映射文件》
- 《MyBatis初始化(三)之SQL初始化(上)》
- 《MyBatis初始化(四)之SQL初始化(下)》
由於在MyBatis的初始化過程中去解析Mapper接口與XML映射文件涉及到的篇幅比較多,XML映射文件的解析過程也比較復雜,所以才分成了后面三個模塊,逐步分析,這樣便於理解
初始化(三)之SQL初始化(上)
在前面的MyBatis初始化相關文檔中已經大致講完了MyBatis初始化的整個流程,其中遺漏了一部分,就是在解析<select /> <insert /> <update /> <delete />
節點的過程中,是如何解析SQL語句,如何實現動態SQL語句,最終會生成一個org.apache.ibatis.mapping.SqlSource
對象的,對於這煩瑣且易出錯的過程,我們來看看MyBatis如何實現的?
我們回顧org.apache.ibatis.builder.xml.XMLStatementBuilder
的parseStatementNode()
解析 Statement 節點時,通過下面的方法創建對應的SqlSource
對象
// 創建對應的 SqlSource 對象,保存了該節點下 SQL 相關信息
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
langDriver
是從Configuration全局配置對象中獲取的默認實現類,對應的也就是XMLLanguageDriver
,在Configuration初始化的時候設置的
public Configuration() {
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
主要包路徑:org.apache.ibatis.scripting、org.apache.ibatis.builder、org.apache.ibatis.mapping
主要涉及到的類:
-
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
:語言驅動接口的默認實現,創建ParameterHandler參數處理器對象和SqlSource資源對象 -
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
:繼承 BaseBuilder 抽象類,負責將SQL腳本(XML或者注解中定義的SQL語句)解析成SqlSource(DynamicSqlSource或者RawSqlSource)資源對象 -
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler
:定義在XMLScriptBuilder
內部的一個接口,用於處理MyBatis自定義標簽(<if /> <foreach />
等),生成對應的SqlNode對象,不同的實現類處理不同的標簽 -
org.apache.ibatis.scripting.xmltags.DynamicContext
:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理后的SQL語句,內部提供ContextMap對象保存上下文的參數 -
org.apache.ibatis.scripting.xmltags.SqlNode
:SQL Node接口,每個XML Node會解析成對應的SQL Node對象,通過上下文可以對動態SQL進行邏輯處理,生成需要的結果 -
org.apache.ibatis.scripting.xmltags.OgnlCache
:用於處理Ognl表達式
語言驅動接口的實現類如下圖所示:

LanguageDriver
org.apache.ibatis.scripting.LanguageDriver
:語言驅動接口,代碼如下:
public interface LanguageDriver {
/**
* Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
* 創建 ParameterHandler 對象
*
* @param mappedStatement The mapped statement that is being executed
* @param parameterObject The input parameter object (can be null)
* @param boundSql The resulting SQL once the dynamic language has been executed.
* @return 參數處理器
* @author Frank D. Martinez [mnesarco]
* @see DefaultParameterHandler
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* Creates an {@link SqlSource} that will hold the statement read from a mapper xml file.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 創建 SqlSource 對象,從 Mapper XML 配置的 Statement 標簽中,即 <select /> 等。
*
* @param configuration The MyBatis configuration
* @param script XNode parsed from a XML file
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return SQL 資源
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* Creates an {@link SqlSource} that will hold the statement read from an annotation.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 創建 SqlSource 對象,從方法注解配置,即 @Select 等。
*
* @param configuration The MyBatis configuration
* @param script The content of the annotation
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return SQL 資源
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
定義了三個方法:
-
createParameterHandler
:獲取 ParameterHandler 參數處理器對象 -
createSqlSource
:創建 SqlSource 對象,解析 Mapper XML 配置的 Statement 標簽中,即<select /> <update /> <delete /> <insert />
-
createSqlSource
:創建 SqlSource 對象,從方法注解配置,即 @Select 等
XMLLanguageDriver
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
:語言驅動接口的默認實現,代碼如下:
public class XMLLanguageDriver implements LanguageDriver {
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 創建 DefaultParameterHandler 對象
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
/**
* 用於解析 XML 映射文件中的 SQL
*
* @param configuration The MyBatis configuration
* @param script XNode parsed from a XML file
* @param parameterType input parameter type got from a mapper method or
* specified in the parameterType xml attribute. Can be
* null.
* @return SQL 資源
*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 創建 XMLScriptBuilder 對象,執行解析
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
/**
* 用於解析注解中的 SQL
*
* @param configuration The MyBatis configuration
* @param script The content of the annotation
* @param parameterType input parameter type got from a mapper method or
* specified in the parameterType xml attribute. Can be
* null.
* @return SQL 資源
*/
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
// <1> 如果是 <script> 開頭,表示是在注解中使用的動態 SQL
if (script.startsWith("<script>")) {
// <1.1> 創建 XPathParser 對象,解析出 <script /> 節點
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
// <2.1> 變量替換
script = PropertyParser.parse(script, configuration.getVariables());
// <2.2> 創建 TextSqlNode 對象
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) { // <2.3.1> 如果是動態 SQL ,則創建 DynamicSqlSource 對象
return new DynamicSqlSource(configuration, textSqlNode);
} else { // <2.3.2> 如果非動態 SQL ,則創建 RawSqlSource 對象
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}
實現了LanguageDriver接口:
-
創建
DefaultParameterHandler
默認參數處理器並返回 -
解析 XML 映射文件中的 SQL,通過創建
XMLScriptBuilder
對象,調用其parseScriptNode()
方法解析 -
解析注解定義的 SQL
- 如果是
<script>
開頭,表示是在注解中使用的動態 SQL,將其轉換成 XNode 然后調用上述方法,不了解的可以看看MyBatis三種動態SQL配置方式 - 先將注解中定義的 SQL 中包含的變量進行轉換,然后創建對應的 SqlSource 對象
- 如果是
RawLanguageDriver
org.apache.ibatis.scripting.defaults.RawLanguageDriver
:繼承了XMLLanguageDriver,在的基礎上增加了是否為靜態SQL語句的校驗,也就是判斷創建的 SqlSource 是否為 RawSqlSource 靜態 SQL 資源
XMLScriptBuilder
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
:繼承 BaseBuilder 抽象類,負責將 SQL 腳本(XML或者注解中定義的 SQL )解析成 SqlSource 對象
構造方法
public class XMLScriptBuilder extends BaseBuilder {
/**
* 當前 SQL 的 XNode 對象
*/
private final XNode context;
/**
* 是否為動態 SQL
*/
private boolean isDynamic;
/**
* SQL 的 Java 入參類型
*/
private final Class<?> parameterType;
/**
* NodeNodeHandler 的映射
*/
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
public XMLScriptBuilder(Configuration configuration, XNode context) {
this(configuration, context, null);
}
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
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());
}
}
在構造函數中會初始化 NodeHandler 處理器,分別用於處理不同的MyBatis自定義的XML標簽,例如<if /> <where /> <foreach />
等標簽
parseScriptNode方法
parseScriptNode()
方法將 SQL 腳本(XML或者注解中定義的 SQL )解析成 SqlSource
對象,代碼如下:
public SqlSource parseScriptNode() {
// 解析 XML 或者注解中定義的 SQL
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 動態語句,使用了 ${} 也算
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
- 通過調用
parseDynamicTags(XNode node)
方法,將解析 SQL 成MixedSqlNode
對象,主要是將一整個 SQL 解析成一系列的 SqlNode 對象 - 如果是動態SQL語句,使用了MyBatis自定義的XML標簽(
<if />
等)或者使用了${}
,則封裝成DynamicSqlSource
對象 - 否則就是靜態SQL語句,封裝成
RawSqlSource
對象
parseDynamicTags方法
parseDynamicTags()
將 SQL 腳本(XML或者注解中定義的 SQL )解析成MixedSqlNode
對象,代碼如下:
protected MixedSqlNode parseDynamicTags(XNode node) {
// <1> 創建 SqlNode 數組
List<SqlNode> contents = new ArrayList<>();
/*
* <2> 遍歷 SQL 節點中所有子節點
* 這里會對該節點內的所有內容進行處理然后返回 NodeList 對象
* 1. 文本內容會被解析成 '<#text></#text>' 節點,就算一個換行符也會解析成這個
* 2. <![CDATA[ content ]]> 會被解析成 '<#cdata-section>content</#cdata-section>' 節點
* 3. 其他動態<if /> <where />
*/
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 當前子節點
XNode child = node.newXNode(children.item(i));
// <2.1> 如果類型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 時
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE // <![CDATA[ ]]>節點
|| child.getNode().getNodeType() == Node.TEXT_NODE) { // 純文本
// <2.1.1> 獲得內容
String data = child.getStringBody("");
// <2.1.2> 創建 TextSqlNode 對象
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) { // <2.1.2.1> 如果是動態的 TextSqlNode 對象,也就是使用了 '${}'
// 添加到 contents 中
contents.add(textSqlNode);
// 標記為動態 SQL
isDynamic = true;
} else { // <2.1.2.2> 如果是非動態的 TextSqlNode 對象,沒有使用 '${}'
// <2.1.2> 創建 StaticTextSqlNode 添加到 contents 中
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 <2.2> 如果類型是 Node.ELEMENT_NODE
// <2.2.1> 根據子節點的標簽,獲得對應的 NodeHandler 對象
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) { // 獲得不到,說明是未知的標簽,拋出 BuilderException 異常
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// <2.2.2> 執行 NodeHandler 處理
handler.handleNode(child, contents);
// <2.2.3> 標記為動態 SQL
isDynamic = true;
}
}
// <3> 創建 MixedSqlNode 對象
return new MixedSqlNode(contents);
}
<1>
創建 SqlNode 數組 contents
,用於保存解析 SQL 后的一些列 SqlNode 對象
<2>
獲取定義的 SQL 節點中所有子節點,返回一個 NodeList 對象,這個對象中包含了該 SQL 節點內的所有信息,然后逐個遍歷子節點
1. 其中文本內容會被解析成`<#text></#text>`節點,就算一個換行符也會解析成這個
2. `<![CDATA[ ]]>` 會被解析成 `<#cdata-section></#cdata-section>` 節點
3. 還有其他MyBatis自定義的標簽`<if /> <where />`等等
<2.1>
如果子節點是<#text />
或者<#cdata-section />
類型
<2.1.1>
獲取子節點的文本內容
<2.1.2>
創建 TextSqlNode 對象
<2.1.2.1>
調用 TextSqlNode
的 isDynamic() 方法,點擊去該進去看看就知道了,如果文本中使用了${}
,則標記為動態 SQL 語句,將其添加至 contents
數組中
<2.1.2.2>
否則就是靜態文本內容,創建對應的 StaticTextSqlNode
對象,將其添加至 contents
數組中
<2.2>
如果類型是 Node.ELEMENT_NODE
時,也就是 MyBatis 的自定義標簽
<2.2.1>
根據子節點的標簽名稱,獲得對應的 NodeHandler
對象
<2.2.2>
執行NodeHandler
的handleNode
方法處理該節點,創建不通類型的 SqlNode 並添加到 contents
數組中,如何處理的在下面講述
<2.2.3>
標記為動態 SQL 語句
<3>
最后將創建 contents
封裝成 MixedSqlNode
對象
NodeHandler
XMLScriptBuilder
的內部接口,用於處理MyBatis自定義標簽,接口實現類如下圖所示:

代碼如下:
private interface NodeHandler {
/**
* 處理 Node
*
* @param nodeToHandle 要處理的 XNode 節點
* @param targetContents 目標的 SqlNode 數組。實際上,被處理的 XNode 節點會創建成對應的 SqlNode 對象,添加到 targetContents 中
*/
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
這些 NodeHandler 實現類都定義在 XMLScriptBuilder 內部,用於處理不同標簽,我們逐個來看
BindHandler
實現了NodeHandler接口,<bind />
標簽的處理器,代碼如下:
/**
* <bind />元素允許你在 OGNL 表達式(SQL語句)以外創建一個變量,並將其綁定到當前的上下文
*/
private class BindHandler implements NodeHandler {
public BindHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析 name、value 屬性
final String name = nodeToHandle.getStringAttribute("name");
final String expression = nodeToHandle.getStringAttribute("value");
// 創建 VarDeclSqlNode 對象
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
-
獲取
<bind />
標簽的name和value屬性 -
根據這些屬性創建一個
VarDeclSqlNode
對象 -
添加到
targetContents
集合中
例如這樣配置:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
TrimHandler
實現了NodeHandler接口,<trim />
標簽的處理器,代碼如下:
private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// <1> 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// <2> 獲得 prefix、prefixOverrides、"suffix"、suffixOverrides 屬性
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
// <3> 創建 TrimSqlNode 對象
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
-
繼續調用
parseDynamicTags
方法解析<if />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 -
獲得
prefix
、prefixOverrides
、suffix
、suffixOverrides
屬性 -
根據上面獲取到的屬性創建
TrimSqlNode
對象 -
添加到
targetContents
集合中
WhereHandler
實現了NodeHandler接口,<where />
標簽的處理器,代碼如下:
private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 創建 WhereSqlNode 對象
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
- 繼續調用
parseDynamicTags
方法解析<where />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 - 創建
WhereSqlNode
對象,該對象繼承了TrimSqlNode
,自定義前綴(WHERE)和需要刪除的前綴(AND、OR等) - 添加到
targetContents
集合中
SetHandler
實現了NodeHandler接口,<set />
標簽的處理器,代碼如下:
private class SetHandler implements NodeHandler {
public SetHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
- 繼續調用
parseDynamicTags
方法解析<set />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 - 創建
SetSqlNode
對象,該對象繼承了TrimSqlNode
,自定義前綴(SET)和需要刪除的前綴和后綴(,) - 添加到
targetContents
集合中
ForEachHandler
實現了NodeHandler接口,<foreach />
標簽的處理器,代碼如下:
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 獲得 collection、item、index、open、close、separator 屬性
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
// 創建 ForEachSqlNode 對象
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
- 繼續調用
parseDynamicTags
方法解析<foreach />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 - 獲得 collection、item、index、open、close、separator 屬性
- 根據這些屬性創建
ForEachSqlNode
對象 - 添加到
targetContents
集合中
IfHandler
實現了NodeHandler接口,<if />
標簽的處理器,代碼如下:
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 獲得 test 屬性
String test = nodeToHandle.getStringAttribute("test");
// 創建 IfSqlNode 對象
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
- 繼續調用
parseDynamicTags
方法解析<if />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 - 獲得 test 屬性
- 根據這個屬性創建
IfSqlNode
對象 - 添加到
targetContents
集合中
OtherwiseHandler
實現了NodeHandler接口,<otherwise />
標簽的處理器,代碼如下:
private class OtherwiseHandler implements NodeHandler {
public OtherwiseHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析內部的 SQL 節點,成 MixedSqlNode 對象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
targetContents.add(mixedSqlNode);
}
}
- 繼續調用
parseDynamicTags
方法解析<otherwise />
標簽內部的子標簽節點,嵌套解析,生成MixedSqlNode對象 - 添加到
targetContents
集合中,需要結合ChooseHandler使用
ChooseHandler
實現了NodeHandler接口,<choose />
標簽的處理器,代碼如下:
private class ChooseHandler implements NodeHandler {
public ChooseHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
// 解析 `<when />` 和 `<otherwise />` 的節點們
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
// 獲得 `<otherwise />` 的節點,存在多個會拋出異常
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
// 創建 ChooseSqlNode 對象
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes,
List<SqlNode> defaultSqlNodes) {
List<XNode> children = chooseSqlNode.getChildren();
for (XNode child : children) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler instanceof IfHandler) { // 處理 `<when />` 標簽的情況
handler.handleNode(child, ifSqlNodes);
} else if (handler instanceof OtherwiseHandler) { // 處理 `<otherwise />` 標簽的情況
handler.handleNode(child, defaultSqlNodes);
}
}
}
private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
SqlNode defaultSqlNode = null;
if (defaultSqlNodes.size() == 1) {
defaultSqlNode = defaultSqlNodes.get(0);
} else if (defaultSqlNodes.size() > 1) {
throw new BuilderException("Too many default (otherwise) elements in choose statement.");
}
return defaultSqlNode;
}
}
-
先逐步處理
<choose />
標簽的<when />
和<otherwise />
子標簽們,通過組合 IfHandler 和 OtherwiseHandler 兩個處理器,實現對子節點們的解析 -
如果存在
<otherwise />
子標簽,則拋出異常 -
根據這些屬性創建
ChooseSqlNode
對象 -
添加到
targetContents
集合中
DynamicContext
org.apache.ibatis.scripting.xmltags.DynamicContext
:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理后的SQL語句,內部提供ContextMap對象保存上下文的參數
構造方法
public class DynamicContext {
/**
* 入參保存在 ContextMap 中的 Key
*
* {@link #bindings}
*/
public static final String PARAMETER_OBJECT_KEY = "_parameter";
/**
* 數據庫編號保存在 ContextMap 中的 Key
*
* {@link #bindings}
*/
public static final String DATABASE_ID_KEY = "_databaseId";
static {
// <1.2> 設置 OGNL 的屬性訪問器
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
/**
* 上下文的參數集合,包含附加參數(通過`<bind />`標簽生成的,或者`<foreach />`標簽中的集合的元素等等)
*/
private final ContextMap bindings;
/**
* 生成后的 SQL
*/
private final StringJoiner sqlBuilder = new StringJoiner(" ");
/**
* 唯一編號。在 {@link org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler} 使用
*/
private int uniqueNumber = 0;
public DynamicContext(Configuration configuration, Object parameterObject) {
// <1> 初始化 bindings 參數
if (parameterObject != null && !(parameterObject instanceof Map)) {
// 構建入參的 MetaObject 對象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
// 入參類型是否有對應的類型處理器
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
// <2> 添加 bindings 的默認值
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
}
類型 | 屬性 | 說明 |
---|---|---|
ContextMap | bindings | 上下文的參數集合,包含附加參數(通過<bind /> 標簽生成的,或者<foreach /> 標簽解析參數保存的),以及幾個默認值 |
StringJoiner | sqlBuilder | 保存本次解析后的SQL,每次添加字符串以空格作為分隔符 |
int | uniqueNumber | 唯一編號,在ForEachHandler 處理節點時需要用到,生成唯一數組作為集合中每個元素的索引(作為后綴) |
- 初始化
bindings
參數,創建 ContextMap 對象- 根據入參轉換成MetaObject對象
- 在靜態代碼塊中,設置OGNL的屬性訪問器,OgnlRuntime 是
OGNL
庫中的類,設置ContextMap對應的訪問器是ContextAccessor
類
- 往
bindings
中添加幾個默認值:_parameter
> 入參對象,_databaseId
-> 數據庫標識符
ContextMap
DynamicContext的內部靜態類,繼承HashMap,用於保存解析動態SQL語句時的上下文的參數集合,代碼如下:
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
/**
* parameter 對應的 MetaObject 對象
*/
private final MetaObject parameterMetaObject;
/**
* 是否有對應的類型處理器
*/
private final boolean fallbackParameterObject;
public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
this.parameterMetaObject = parameterMetaObject;
this.fallbackParameterObject = fallbackParameterObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject == null) {
return null;
}
if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
return parameterMetaObject.getOriginalObject();
} else {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
}
}
重寫了 HashMap 的 get(Object key) 方法,增加支持對 parameterMetaObject
屬性的訪問
ContextAccessor
DynamicContext的內部靜態類,實現 ognl.PropertyAccessor
接口,上下文訪問器,代碼如下:
static class ContextAccessor implements PropertyAccessor {
@Override
public Object getProperty(Map context, Object target, Object name) {
Map map = (Map) target;
// 優先從 ContextMap 中,獲得屬性
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
// <x> 如果沒有,則從 PARAMETER_OBJECT_KEY 對應的 Map 中,獲得屬性
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map) parameterObject).get(name);
}
return null;
}
@Override
public void setProperty(Map context, Object target, Object name, Object value) {
Map<Object, Object> map = (Map<Object, Object>) target;
map.put(name, value);
}
@Override
public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
@Override
public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
}
在DynamicContext的靜態代碼塊中,設置OGNL
的屬性訪問器,設置了ContextMap.class的屬性訪問器為ContextAccessor
這里方法的入參中的target
,就是 ContextMap 對象
-
在重寫的
getProperty
方法中,先從 ContextMap 里面獲取屬性值(可以回過去看下ContextMap的get方法) -
沒有獲取到則獲取
PARAMETER_OBJECT_KEY
屬性的值,如果是 Map 類型,則從這里面獲取屬性值
回看 DynamicContext 的構造方法,細品一下😄😄😄,先從Map中獲取屬性值,沒有獲取到則從parameterObject
入參對象中獲取屬性值
SqlNode
org.apache.ibatis.scripting.xmltags.SqlNode
:SQL Node接口,每個XML Node會解析成對應的SQL Node對象,通過上下文可以對動態SQL進行邏輯處理,生成需要的結果
實現類如下圖所示:

代碼如下:
public interface SqlNode {
/**
* 應用當前 SQLNode 節點
*
* @param context 正在解析 SQL 語句的上下文
* @return 是否應用成功
*/
boolean apply(DynamicContext context);
}
因為在解析SQL語句的時候我們需要根據入參來處理不同的SqlNode,通過其apply(DynamicContext context)
方法應用SqlNode節點,將節點轉換成相應的SQL
我們來看看它的實現類是如何處理相應的SQL Node的
VarDeclSqlNode
org.apache.ibatis.scripting.xmltags.VarDeclSqlNode
:實現 SqlNode 接口,<bind />
標簽對應的 SqlNode 實現類,代碼如下:
public class VarDeclSqlNode implements SqlNode {
/**
* 變量名稱
*/
private final String name;
/**
* 表達式
*/
private final String expression;
public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
}
@Override
public boolean apply(DynamicContext context) {
// 獲取該表達式轉換后結果
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 將該結果與變量名稱設置到解析 SQL 語句的上下文中,這樣接下來的解析過程中可以獲取到 name 的值
context.bind(name, value);
return true;
}
}
-
通過
OGNL
表達式expression
從DynamicContext上下文的ContextMap中獲取轉換后的結果,OgnlCache
在后面講到 -
將
name
與轉換后的結果綁定到DynamicContext上下文中,后續處理其他節點可以獲取到
TrimSqlNode
org.apache.ibatis.scripting.xmltags.TrimSqlNode
:實現 SqlNode 接口,<trim/>
標簽對應的 SqlNode 實現類
構造方法
public class TrimSqlNode implements SqlNode {
/**
* MixedSqlNode,包含該<if />節點內所有信息
*/
private final SqlNode contents;
/**
* 前綴,行首添加
*/
private final String prefix;
/**
* 后綴,行尾添加
*/
private final String suffix;
/**
* 需要刪除的前綴,例如這樣定義:'AND|OR'
* 注意空格,這里是不會去除的
*/
private final List<String> prefixesToOverride;
/**
* 需要刪除的后綴,例如我們這樣定義:',|AND'
* 注意空格,這里是不會去除的
*/
private final List<String> suffixesToOverride;
private final Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride,
String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride,
String suffix, List<String> suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
}
在構造方法中解析<trim />
標簽的屬性,其中調用了parseOverrides
方法將|
作為分隔符分隔該字符串並全部大寫,生成一個數組,相關屬性可查看上面的注釋
apply方法
@Override
public boolean apply(DynamicContext context) {
// <1> 創建 FilteredDynamicContext 對象
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// <2> 先解析 <trim /> 節點中的內容,將生成的 SQL 先存放在 FilteredDynamicContext 中
boolean result = contents.apply(filteredDynamicContext);
/*
* <3> 執行 FilteredDynamicContext 的應用
* 對上一步解析到的內容進行處理
* 處理完成后再將處理后的 SQL 拼接到 DynamicContext 中
*/
filteredDynamicContext.applyAll();
return result;
}
-
通過裝飾器模式將
context
裝飾成FilteredDynamicContext
對象 -
因為
<trim />
標簽中定義了內容或者其他標簽,都會解析成相應的SqlNode,保存在contents
中(MixedSqlNode)所以這里需要先應用內部的SqlNode,轉換后的SQL會先保存在FilteredDynamicContext中
-
對
FilteredDynamicContext
中的SQL進行處理,也就是添加前后綴,去除前后綴的處理邏輯,然后將處理后的SQL拼接到context
中
FilteredDynamicContext
TrimSqlNode的私有內部類,繼承了DynamicContext類,對<trim />
標簽邏輯的實現,代碼如下:
private class FilteredDynamicContext extends DynamicContext {
/**
* 裝飾的 DynamicContext 對象
*/
private DynamicContext delegate;
/**
* 是否 prefix 已經被應用
*/
private boolean prefixApplied;
/**
* 是否 suffix 已經被應用
*/
private boolean suffixApplied;
/**
* StringBuilder 對象
*
* @see #appendSql(String)
*/
private StringBuilder sqlBuffer;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
public void applyAll() {
// <1> 去除前后多余的空格,生成新的 sqlBuffer 對象
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
// <2> 全部大寫
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
// <3> 應用 TrimSqlNode 的 trim 邏輯
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
// prefixesToOverride 非空,先刪除
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// prefix 非空,再添加
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
// suffixesToOverride 非空,先刪除
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
// suffix 非空,再添加
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
邏輯並不復雜,大家可以看下
WhereSqlNode
org.apache.ibatis.scripting.xmltags.WhereSqlNode
:繼承了TrimSqlNode
類,<where />
標簽對應的 SqlNode 實現類,代碼如下:
public class WhereSqlNode extends TrimSqlNode {
/**
* 也是通過 TrimSqlNode ,這里定義需要刪除的前綴
*/
private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
// 設置前綴和需要刪除的前綴
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
基於TrimSqlNode
類,定義了需要添加的前綴為WHERE
和需要刪除的前綴AND
和OR
SetSqlNode
org.apache.ibatis.scripting.xmltags.SetSqlNode
:繼承了TrimSqlNode
類,<set />
標簽對應的 SqlNode 實現類,代碼如下:
public class SetSqlNode extends TrimSqlNode {
/**
* 也是通過 TrimSqlNode ,這里定義需要刪除的前綴
*/
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
// 設置前綴、需要刪除的前綴和后綴
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}
基於TrimSqlNode
類,定義了需要添加的前綴為SET
、需要刪除的前綴,
和需要刪除的后綴,
ForeachNode
org.apache.ibatis.scripting.xmltags.ForeachNode
:實現 SqlNode 接口,<foreach />
標簽對應的 SqlNode 實現類
其中apply(DynamicContext context)
方法的處理邏輯饒了我半天,大家可以仔細看一下
構造方法
public class ForEachSqlNode implements SqlNode {
/**
* 集合中元素綁定到上下文中 key 的前綴
*/
public static final String ITEM_PREFIX = "__frch_";
/**
* 表達式計算器
*/
private final ExpressionEvaluator evaluator;
/**
* 需要遍歷的集合類型,支持:list set map array
*/
private final String collectionExpression;
/**
* MixedSqlNode,包含該<where />節點內所有信息
*/
private final SqlNode contents;
/**
* 開頭
*/
private final String open;
/**
* 結尾
*/
private final String close;
/**
* 每個元素以什么分隔
*/
private final String separator;
/**
* 集合中每個元素的值
*/
private final String item;
/**
* 集合中每個元素的索引
*/
private final String index;
private final Configuration configuration;
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index,
String item, String open, String close, String separator) {
this.evaluator = new ExpressionEvaluator();
this.collectionExpression = collectionExpression;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
}
對每個屬性進行賦值,參考每個屬性上面的注釋
apply方法
@Override
public boolean apply(DynamicContext context) {
// 獲取入參
Map<String, Object> bindings = context.getBindings();
/*
* <1> 獲得遍歷的集合的 Iterable 對象,用於遍歷
* 例如配置了 collection 為以下類型
* list:則從入參中獲取到 List 集合類型的屬性的值
* array:則從入參中獲取到 Array 數組類型的屬性的值,會轉換成 ArrayList
* map:則從入參中獲取到 Map 集合類型的屬性的值
*/
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
// 集合中沒有元素則無需遍歷
return true;
}
boolean first = true;
// <2> 添加 open 到 SQL 中
applyOpen(context);
int i = 0;
for (Object o : iterable) {
// <3> 記錄原始的 context 對象,下面通過兩個裝飾器對他進行操作
DynamicContext oldContext = context;
// <4> 生成一個 context 裝飾器
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
// 設置其需要添加的前綴為分隔符
context = new PrefixedContext(context, separator);
}
// <5> 生成一個唯一索引值
int uniqueNumber = context.getUniqueNumber();
// Issue #709
// <6> 綁定到 context 中
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
/*
* 和下面同理,只不過索引是 Map 的 key
*/
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
/*
* 綁定當前集合中當前元素的索引到當前解析 SQL 語句的上下文中
*
* 1. 'index' -> i
*
* 2. __frch_'index'_uniqueNumber -> i
*/
applyIndex(context, i, uniqueNumber);
/*
* 綁定集合中當前元素的值到當前解析 SQL 語句的上下文中
*
* 1. 'item' -> o
*
* 2. __frch_'item'_uniqueNumber -> o
*
*/
applyItem(context, o, uniqueNumber);
}
/*
* 再裝飾一下 PrefixedContext -> FilteredDynamicContext
*
* 前者進行前綴的添加,第一個元素添加后設置為已添加標記,后續不在添加
* 后者將<foreach />標簽內的"#{item}"或者"#{index}"替換成上面我們已經綁定的數據:"#{__frch_'item'_uniqueNumber}"或者"#{__frch_'index'_uniqueNumber}"
*
* <7> 進行轉換,將<foreach />標簽內部定義的內容進行轉換
*/
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) { // <8> 判斷 prefix 是否已經插入
first = !((PrefixedContext) context).isPrefixApplied();
}
// <9> 恢復原始的 context 對象,因為目前 context 是裝飾器
context = oldContext;
i++;
}
// <10> 添加 close 到 SQL 中
applyClose(context);
// <11> 移除 index 和 item 對應的綁定
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}
private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + "_" + i;
}
-
獲得需要遍歷的集合 Iterable 對象,調用
ExpressionEvaluator
的evaluateIterable(String expression, Object parameterObject)
方法,根據表達式從參數中獲取集合對象- 先通過
OgnlCache
根據Ognl表達式從上下文的ContextMap中獲取轉換后的結果,OgnlCache
在后面會講到 - 如果是Array數組類型,則轉換成ArrayList集合后返回
- 如果是Map類型,則調用Map.Entry的集合
- 先通過
-
如果定義了
open
屬性,則先拼接到SQL中 -
開始遍歷集合 Iterable 對象,先記錄
context
原始對象為oldContext
,因為接下來需要對其進行兩次裝飾,而這里會再次進入 -
創建一個
PrefixedContext
對象,裝飾context
,主要是對集合中的每個元素添加separator
分隔符 -
生成一個唯一索引值,也就是DynamicContext的
uniqueNumber++
,這樣集合中每個元素都有一個唯一索引了 -
將集合中的當前元素綁定到上下文中,會保存以下信息:
applyIndex
:如果配置了index
屬性,則將當前元素的索引值綁定到上下文的ContextMap中,保存兩個數據:-
'index'
-> i,其中'index'
就是我們在<foreach />
標簽中配置的index屬性,i
就是當前元素在集合中的索引 -
__frch_'index'_uniqueNumber
-> i
applyItem
:如果配置了item
屬性,則將當前元素綁定到上下文的ContextMap中,保存兩個數據:'item'
-> o,其中'item'
就是我們在<foreach />
標簽中配置的item屬性,o
就是當前元素對象__frch_'item'_uniqueNumber
-> o
-
-
再將
PrefixedContext
對象裝飾成FilteredDynamicContext
對象然后應用
<foreach />
標簽內部的SqlNode節點們主要是替換我們在
<foreach />
標簽中定義的內容,替換成上面第6
步綁定的數據的key值,這樣就可以獲取到該key對應的value了例如:將
<foreach />
標簽內的#{item}或者#{index}替換成第6
步已經綁定的數據的key值#{__frch_'item'_uniqueNumber}
或者#{__frch_'index'_uniqueNumber}
,然后拼接到SQL中 -
判斷是否添加了
open
前綴,添加了則遍歷時不用再添加前綴 -
恢復原始的
oldContext
對象,因為目前context
是裝飾器,然后繼續遍歷 -
如果定義了
close
屬性,則拼接到SQL中 -
從上下文的ContextMap中移除第
6
步綁定的第1條數據
第6
步中,如果是Map類型,i
對應的就是key值,o
對應的就是value值,為什么兩個方法都需要保存第1條數據?
因為<foreach />
標簽中可能還有其他的標簽,例如<if />
標簽,它的判斷條件中可能需要用到當前元素或者索引值,而表達式中使用了'index'
或者'item'
,那么就需要從上下文中獲取到對應的值了
那么接下來我們來看看內部定義的兩個類:PrefixedContext和FilteredDynamicContext
PrefixedContext
ForeachNode的內部類,繼承了DynamicContext,用於應用<foreach />
標簽時添加分隔符
重寫了appendSql方法,邏輯比較簡單,判斷是否需要添加分隔符,代碼如下:
private class PrefixedContext extends DynamicContext {
/**
* 裝飾的 DynamicContext 對象
*/
private final DynamicContext delegate;
/**
* 需要添加的前綴
*/
private final String prefix;
/**
* 是否已經添加
*/
private boolean prefixApplied;
public PrefixedContext(DynamicContext delegate, String prefix) {
super(configuration, null);
this.delegate = delegate;
this.prefix = prefix;
this.prefixApplied = false;
}
public boolean isPrefixApplied() {
return prefixApplied;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
delegate.appendSql(prefix);
prefixApplied = true;
}
delegate.appendSql(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
FilteredDynamicContext
ForeachNode的私有靜態內部類,繼承了DynamicContext,用於應用<foreach />
標簽時替換內部的#{item}或者#{index},
重寫了appendSql方法,代碼如下:
private static class FilteredDynamicContext extends DynamicContext {
/**
* 裝飾的對象
*/
private final DynamicContext delegate;
/**
* 集合中當前元素的索引
*/
private final int index;
/**
* <foreach />定義的 index 屬性
*/
private final String itemIndex;
/**
* <foreach />定義的 item 屬性
*/
private final String item;
public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
// 如果在`<foreach />`標簽下的內容為通過item獲取元素,則替換成`__frch_'item'_uniqueNumber`
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
/*
* 如果在`<foreach />`標簽中定義了index屬性,並且標簽下的內容為通過index獲取元素
* 則替換成`__frch_'index'_uniqueNumber`
*/
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
/*
* 返回`#{__frch_'item'_uniqueNumber}`或者`#{__frch_'index'_uniqueNumber}`
* 因為在前面已經將集合中的元素綁定在上下文的ContextMap中了,所以可以通過上面兩個key獲取到對應元素的值
* 例如綁定的數據:
* 1. __frch_'item'_uniqueNumber = 對應的元素值
* 2. __frch_'index'_uniqueNumber = 對應的元素值的索引
*/
return "#{" + newContent + "}";
});
delegate.appendSql(parser.parse(sql));
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
-
創建一個GenericTokenParser對象
parser
,用於處理#{}
-
創建一個TokenHandler處理器,大致的處理邏輯:
- 如果在
<foreach />
標簽下的內容為通過item獲取元素,則替換成__frch_'item'_uniqueNumber
- 如果在
<foreach />
標簽中定義了index屬性,並且標簽下的內容為通過index獲取元素,則替換成__frch_'index'_uniqueNumber
- 返回
#{__frch_'item'_uniqueNumber}
或者#{__frch_'index'_uniqueNumber}
,因為在前面已經將集合中的元素綁定在上下文的ContextMap中了,所以可以通過上面兩個key獲取到對應元素的值
- 如果在
-
調用
parser
進行解析,使用第2
創建處理器進行處理,然后將轉換后的結果拼接到SQL中
IfSqlNode
org.apache.ibatis.scripting.xmltags.IfSqlNode
:實現 SqlNode 接口,<if />
標簽對應的 SqlNode 實現類,代碼如下:
public class IfSqlNode implements SqlNode {
/**
* 表達式計算器
*/
private final ExpressionEvaluator evaluator;
/**
* 判斷條件的表達式
*/
private final String test;
/**
* MixedSqlNode,包含該<if />節點內所有信息
*/
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// <1> 判斷是否符合條件
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// <2> 解析該<if />節點中的內容
contents.apply(context);
return true;
}
// <3> 不符合
return false;
}
}
-
調用
ExpressionEvaluator
的evaluateBoolean(String expression, Object parameterObject)
方法,根據表達式從參數中獲取結果- 先通過
OgnlCache
根據Ognl表達式從上下文的ContextMap中獲取轉換后的結果,OgnlCache
在后面會講到 - 如果是Boolean,則轉換成Boolean類型返回
- 如果是Number類型,則判斷是否不等於 0
- 其他類則判斷是否不等於null
- 先通過
-
根據第
1
步的結果判斷是否應用<if />
標簽內的SqlNode節點們
ChooseSqlNode
org.apache.ibatis.scripting.xmltags.ChooseSqlNode
:實現 SqlNode 接口,<choose />
標簽對應的 SqlNode 實現類,代碼如下:
public class ChooseSqlNode implements SqlNode {
/**
* <otherwise /> 標簽對應的 SqlNode 節點
*/
private final SqlNode defaultSqlNode;
/**
* <when /> 標簽對應的 SqlNode 節點數組
*/
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
// <1> 先判斷 <when /> 標簽中,是否有符合條件的節點。
// 如果有,則進行應用。並且只因應用一個 SqlNode 對象
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// <2> 再判斷 <otherwise /> 標簽,是否存在
// 如果存在,則進行應用
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
// <3> 返回都失敗
return false;
}
}
- 先應用
<choose />
下的所有<when />
標簽所對應的IfSqlNode,有一個應用成功則返回true - 如果所有的
<when />
都不滿足條件,則應用<otherwise />
標簽下的內容所對應的SqlNode
StaticTextSqlNode
org.apache.ibatis.scripting.xmltags.StaticTextSqlNode
:實現 SqlNode 接口,用於保存靜態文本,邏輯比較簡單,直接拼接文本,代碼如下:
public class StaticTextSqlNode implements SqlNode {
/**
* 靜態內容
*/
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
// 直接往正在解析 SQL 語句的上下文的 SQL 中添加該內容
context.appendSql(text);
return true;
}
}
TextSqlNode
org.apache.ibatis.scripting.xmltags.TextSqlNode
:實現了 SqlNode 接口,用於處理${}
,注入對應的值,代碼如下:
public class TextSqlNode implements SqlNode {
/**
* 動態文本
*/
private final String text;
/**
* 注入時的過濾器
*/
private final Pattern injectionFilter;
public TextSqlNode(String text) {
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
public boolean isDynamic() {
// <1> 創建 DynamicCheckerTokenParser 對象
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// <2> 創建 GenericTokenParser 對象
GenericTokenParser parser = createParser(checker);
// <3> 執行解析,如果存在 '${ }',則 checker 會設置 isDynamic 為true
parser.parse(text);
// <4> 判斷是否為動態文本
return checker.isDynamic();
}
@Override
public boolean apply(DynamicContext context) {
// <1> 創建 BindingTokenParser 對象
// <2> 創建 GenericTokenParser 對象
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// <3> 執行解析
// <4> 將解析的結果,添加到 context 中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
/**
* 注入時的過濾器
*/
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
// 從上下文中獲取入參對象,在DynamicContext的構造方法中可以看到為什么可以獲取到
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 使用 OGNL 表達式,獲得對應的值
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
// 使用過濾器進行過濾
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
}
在XML文件中編寫SQL語句時,如果使用到了${}
作為變量時,那么會生成TextSqlNode對
象,可以回看XMLScriptBuilder
的parseDynamicTags()
方法
在MyBatis處理SQL語句時就會將
${}
進行替換成對應的參數,存在SQL注入的安全性問題而
#{}
就不一樣了,MyBatis會將其替換成?
占位符,通過java.sql.PreparedStatement
進行預編譯處理,不存在上面的問題
- 創建GenericTokenParser對象
parser
,用於處理${}
,設置的Token處理器為BindingTokenParser
- 執行解析,我們可以看到BindingTokenParser的
handleToken(String content)
方法- 從上下文中獲取入參對象,在DynamicContext的構造方法中可以看到為什么可以獲取到
- 在將入參對象綁定到上下文中,設置key為"value",為什么這么做呢??沒有仔細探究,可能跟
OGNL
相關,感興趣的小伙伴可以探討一下😈 - 使用 OGNL 表達式,從上下文中獲得
${}
中內容對應的值,如果為null則設置為空字符串 - 使用注入過濾器對注入的值過濾
- 將解析后的結果拼接到SQL中
MixedSqlNode
org.apache.ibatis.scripting.xmltags.MixedSqlNode
:實現 SqlNode 接口,用於保存多個SqlNode對象
因為一個SQL語句會被解析成多個SqlNode,且內部還會嵌套多個,所以使用MixedSqlNode進行保存,代碼如下:
public class MixedSqlNode implements SqlNode {
/**
* 動態節點集合
*/
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 逐個應用
contents.forEach(node -> node.apply(context));
return true;
}
}
OgnlCache
org.apache.ibatis.scripting.xmltags.OgnlCache
:用於處理Ognl表達式
在上面SqlNode的apply方法中,使用到的邏輯判斷時獲取表達式的結果則需要通過OgnlCache來進行解析
對OGNL不了解的小伙伴可以看下這篇文章:Ognl表達式基本原理和使用方法
代碼如下:
public final class OgnlCache {
/**
* OgnlMemberAccess 單例,用於修改某個對象的成員為可訪問
*/
private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess();
/**
* OgnlClassResolver 單例,用於創建 Class 對象
*/
private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver();
/**
* 表達式的緩存的映射
*
* KEY:表達式 VALUE:表達式的緩存 @see #parseExpression(String)
*/
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
private OgnlCache() {
// Prevent Instantiation of Static Class
}
public static Object getValue(String expression, Object root) {
try {
/*
* <1> 創建 OgnlContext 對象,設置 OGNL 的成員訪問器和類解析器,設置根元素為 root 對象
* 這里是調用 OgnlContext 的s etRoot 方法直接設置根元素,可以通過 'user.id' 獲取結果
* 如果是通過 put 方法添加的對象,則取值時需要使用'#',例如 '#user.id'
*/
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
/*
* <2> expression 轉換成 Ognl 表達式
* <3> 根據 Ognl 表達式獲取結果
*/
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
/**
* 根據表達式構建一個 Ognl 表達式
*
* @param expression 表達式,例如<if test="user.id > 0"> </if>,那這里傳入的就是 "user.id > 0"
* @return Ognl 表達式
* @throws OgnlException 異常
*/
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
}
getValue
方法:根據Ognl表達式從Object中獲取結果
-
創建 OgnlContext 對象,設置 OGNL 的成員訪問器和類解析器,設置根元素為
root
對象 -
將創建
expression
轉換成 Ognl 表達式,緩存起來 -
根據 Ognl 表達式從
root
對象中獲取結果
總結
本文講述的是XML映射文件中的<select /> <insert /> <update /> <delete />
節點內的SQL語句如何被解析的
在XMLLanguageDriver
語言驅動類中,通過XMLScriptBuilder
對該到節點的內容進行解析,創建相應的SqlSource
資源對象
在其解析的過程會根據不同的NodeHandler
節點處理器對MyBatis自定義的標簽(<if /> <foreach />
等)進行處理,生成相應的SqlNode
對象,最后將所有的SqlNode
對象存放在MixedSqlNode
中
解析的過程中會判斷是否為動態的SQL語句,包含了MyBatis自定義的標簽或者使用了${}
都是動態的SQL語句,動態的SQL語句創建DynamicSqlSource
對象,否則創建RawSqlSource
對象
那么關於SqlSource
是什么其實這里還不是特別了解,由於其涉及到的篇幅並不少,所以另起一篇文檔《MyBatis初始化(四)之SQL初始化(下)》進行分析
參考文章:芋道源碼《精盡 MyBatis 源碼分析》