該系列文檔是本人在學習 Mybatis 的源碼過程當中總結下來的,可能對讀者不太友好,請結合個人源碼註釋(Mybatis源碼分析 GitHub 地址、Mybatis-Spring 源碼分析 GitHub 地址、Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀html
MyBatis 版本:3.5.2java
MyBatis-Spring 版本:2.0.3node
MyBatis-Spring-Boot-Starter 版本:2.1.4git
在MyBatis初始化過程當中,大體會有如下幾個步驟:github
建立Configuration
全局配置對象,會往TypeAliasRegistry
別名註冊中心添加Mybatis須要用到的相關類,並設置默認的語言驅動類爲XMLLanguageDriver
spring
加載mybatis-config.xml
配置文件、Mapper接口中的註解信息和XML映射文件,解析後的配置信息會造成相應的對象並保存到Configuration全局配置對象中sql
構建DefaultSqlSessionFactory
對象,經過它能夠建立DefaultSqlSession
對象,MyBatis中SqlSession
的默認實現類數據庫
由於整個初始化過程涉及到的代碼比較多,因此拆分紅了四個模塊依次對MyBatis的初始化進行分析:express
因爲在MyBatis的初始化過程當中去解析Mapper接口與XML映射文件涉及到的篇幅比較多,XML映射文件的解析過程也比較複雜,因此才分紅了後面三個模塊,逐步分析,這樣便於理解apache
在前面的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表達式
語言驅動接口的實現類以下圖所示:
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 等
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配置方式org.apache.ibatis.scripting.defaults.RawLanguageDriver
:繼承了XMLLanguageDriver,在的基礎上增長了是否爲靜態SQL語句的校驗,也就是判斷建立的 SqlSource 是否爲 RawSqlSource 靜態 SQL 資源
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()
方法將 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 對象<if />
等)或者使用了${}
,則封裝成DynamicSqlSource
對象RawSqlSource
對象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
對象
XMLScriptBuilder
的內部接口,用於處理MyBatis自定義標籤,接口實現類以下圖所示:
代碼以下:
private interface NodeHandler { /** * 處理 Node * * @param nodeToHandle 要處理的 XNode 節點 * @param targetContents 目標的 SqlNode 數組。實際上,被處理的 XNode 節點會建立成對應的 SqlNode 對象,添加到 targetContents 中 */ void handleNode(XNode nodeToHandle, List<SqlNode> targetContents); }
這些 NodeHandler 實現類都定義在 XMLScriptBuilder 內部,用於處理不一樣標籤,咱們逐個來看
實現了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>
實現了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
集合中
實現了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
集合中實現了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
集合中實現了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對象ForEachSqlNode
對象targetContents
集合中實現了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對象IfSqlNode
對象targetContents
集合中實現了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使用實現了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
集合中
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 對象
OGNL
庫中的類,設置ContextMap對應的訪問器是ContextAccessor
類bindings
中添加幾個默認值:_parameter
> 入參對象,_databaseId
-> 數據庫標識符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
屬性的訪問
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
入參對象中獲取屬性值
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的
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上下文中,後續處理其餘節點能夠獲取到
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
方法將|
做爲分隔符分隔該字符串並所有大寫,生成一個數組,相關屬性可查看上面的註釋
@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
中
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); } } } }
邏輯並不複雜,你們能夠看下
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
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
、須要刪除的前綴,
和須要刪除的後綴,
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; } }
對每一個屬性進行賦值,參考每一個屬性上面的註釋
@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
在後面會講到若是定義了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
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(); } }
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中
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
在後面會講到根據第1
步的結果判斷是否應用<if />
標籤內的SqlNode節點們
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 />
標籤下的內容所對應的SqlNodeorg.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; } }
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
進行預編譯處理,不存在上面的問題
parser
,用於處理${}
,設置的Token處理器爲BindingTokenParser
handleToken(String content)
方法
OGNL
相關,感興趣的小夥伴能夠探討一下😈${}
中內容對應的值,若是爲null則設置爲空字符串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; } }
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 源碼分析》