精盡MyBatis原始碼分析 - MyBatis初始化(四)之 SQL 初始化(下)
摘自:https://www.cnblogs.com/lifullmoon/p/14015075.html
該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(、、)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的初始化
在MyBatis初始化過程中,大致會有以下幾個步驟:
-
建立
Configuration
全域性配置物件,會往TypeAliasRegistry
別名註冊中心新增Mybatis需要用到的相關類,並設定預設的語言驅動類為XMLLanguageDriver
-
載入
mybatis-config.xml
配置檔案、Mapper介面中的註解資訊和XML對映檔案,解析後的配置資訊會形成相應的物件並儲存到Configuration全域性配置物件中 -
構建
DefaultSqlSessionFactory
物件,通過它可以建立DefaultSqlSession
物件,MyBatis中SqlSession
的預設實現類
因為整個初始化過程涉及到的程式碼比較多,所以拆分成了四個模組依次對MyBatis的初始化進行分析:
由於在MyBatis的初始化過程中去解析Mapper介面與XML對映檔案涉及到的篇幅比較多,XML對映檔案的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解
初始化(四)之SQL初始化(下)
在上一篇文件中詳細地講述了MyBatis在解析<select /> <insert /> <update /> <delete />
節點的過程中,是如何解析SQL語句的,如何實現動態SQL語句的,最終會生成一個org.apache.ibatis.mapping.SqlSource
物件的,那麼接下來我們來看看SqlSource
到底是什麼
主要包路徑:org.apache.ibatis.mapping、org.apache.ibatis.builder
主要涉及到的類:
org.apache.ibatis.builder.SqlSourceBuilder
SqlSource
構建器,負責將SQL語句中的#{}
替換成相應的?
佔位符,並獲取該?
佔位符對應的ParameterMapping
物件org.apache.ibatis.builder.ParameterExpression
:繼承了HashMap<String, String>
,引數表示式處理器,在SqlSourceBuilder
處理#{}
的內容時,需要通過其解析成key-value鍵值對org.apache.ibatis.mapping.ParameterMapping
:儲存#{}
中配置的屬性引數資訊org.apache.ibatis.mapping.SqlSource
:SQL 資源介面,用於建立BoundSql物件(包含可執行的SQL語句與引數資訊)org.apache.ibatis.mapping.BoundSql
:用於資料庫可執行的SQL語句的最終封裝物件org.apache.ibatis.scripting.defaults.DefaultParameterHandler
:實現了ParameterHandler介面,用於將入參設定到java.sql.PreparedStatement
預編譯物件中
用於將入參設定到java.sql.PreparedStatement
預編譯物件中
我們先來回顧一下org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
的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; }- 如果是動態 SQL 語句,使用了 MyBatis 的自定義標籤(
<if /> <foreach />
等)或者使用了${}
都是動態 SQL 語句,則會建立DynamicSqlSource
物件 - 否則就是靜態 SQL 語句,建立
RawSqlSource
物件
SqlSource
介面的實現類如下圖所示:
SqlSourceBuilder
org.apache.ibatis.builder.SqlSourceBuilder
:繼承了BaseBuilder抽象類,SqlSource
構建器,負責將SQL語句中的#{}
替換成相應的?
佔位符,並獲取該?
佔位符對應的 org.apache.ibatis.mapping.ParameterMapping
物件
構造方法
public class SqlSourceBuilder extends BaseBuilder { private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; public SqlSourceBuilder(Configuration configuration) { super(configuration); } }其中PARAMETER_PROPERTIES
字串定義了#{}
中支援定義哪些屬性,在拋異常的時候用到
parse方法
解析原始的SQL(僅包含#{}
定義的引數),轉換成StaticSqlSource物件
因為在DynamicSqlSource
呼叫該方法前會將MixedSqlNode
進行處理,呼叫其apply
方法進行應用,根據DynamicContext
上下文對MyBatis的自定義標籤或者包含${}
的SQL生成的SqlNode
進行邏輯處理或者注入值,生成一個SQL(僅包含#{}
定義的引數)
程式碼如下:
/** * 執行解析原始 SQL ,成為 SqlSource 物件 * * @param originalSql 原始 SQL * @param parameterType 引數型別 * @param additionalParameters 上下文的引數集合,包含附加引數集合(通過 <bind /> 標籤生成的,或者`<foreach />`標籤中的集合的元素) * RawSqlSource傳入空集合 * DynamicSqlSource傳入 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合 * @return SqlSource 物件 */ public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // <1> 建立 ParameterMappingTokenHandler 物件 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // <2> 建立 GenericTokenParser 物件 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); /* * <3> 執行解析 * 將我們在 SQL 定義的所有佔位符 #{content} 都替換成 ? * 並生成對應的 ParameterMapping 物件儲存在 ParameterMappingTokenHandler 中 */ String sql = parser.parse(originalSql); // <4> 建立 StaticSqlSource 物件 return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }該方法的入參originalSql
為原始的SQL,也就是其所有的SqlNode節點已經應用了,也就是都呼叫了apply
方法
包含的${}
也已經注入了對應的值,所以這裡只剩#{}
定義的入參了
- 建立
ParameterMappingTokenHandler
處理器物件handler
- 建立GenericTokenParser物件,用於處理
#{}
中的內容,通過handler
將其轉換成?
佔位符,並建立對應的ParameterMapping
物件 - 執行解析,獲取最終的 SQL 語句
- 建立
StaticSqlSource
物件
ParameterMappingTokenHandler
org.apache.ibatis.builder.SqlSourceBuilder
的內部類,用於解析#{}
的內容,建立ParameterMapping
物件,並將其替換成?
佔位符
程式碼如下:
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { /** * 我們在 SQL 語句中定義的佔位符對應的 ParameterMapping 陣列,根據順序來的 */ private List<ParameterMapping> parameterMappings = new ArrayList<>(); /** * 引數型別 */ private Class<?> parameterType; /** * additionalParameters 引數的對應的 MetaObject 物件 */ private MetaObject metaParameters; public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) { super(configuration); this.parameterType = parameterType; // 建立 additionalParameters 引數的對應的 MetaObject 物件 this.metaParameters = configuration.newMetaObject(additionalParameters); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } @Override public String handleToken(String content) { // <1> 構建 ParameterMapping 物件,並新增到 parameterMappings 中 parameterMappings.add(buildParameterMapping(content)); // <2> 返回 ? 佔位符 return "?"; } /** * 根據內容構建一個 ParameterMapping 物件 * * @param content 我們在 SQL 語句中定義的佔位符 * @return ParameterMapping 物件 */ private ParameterMapping buildParameterMapping(String content) { // <1> 將字串解析成 key-value 鍵值對儲存 // 其中有一個key為"property",value就是對應的屬性名稱 Map<String, String> propertiesMap = parseParameterMapping(content); // <2> 獲得屬性的名字和型別 String property = propertiesMap.get("property"); // 名字 Class<?> propertyType; // 型別 if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params propertyType = metaParameters.getGetterType(property); } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { // 有對應的型別處理器,例如java.lang.string propertyType = parameterType; } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { // 設定的 Jdbc Type 是遊標 propertyType = java.sql.ResultSet.class; } else if (property == null || Map.class.isAssignableFrom(parameterType)) { // 是 Map 集合 propertyType = Object.class; } else { // 類物件 MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); if (metaClass.hasGetter(property)) { // 通過反射獲取到其對應的 Java Type propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } // <3> 建立 ParameterMapping.Builder 構建者物件 ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); // <3.1> 初始化 ParameterMapping.Builder 物件的屬性 Class<?> javaType = propertyType; String typeHandlerAlias = null; // 遍歷 SQL 配置的佔位符資訊,例如這樣配置:"name = #{name, jdbcType=VARCHAR}" for (Map.Entry<String, String> entry : propertiesMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if ("javaType".equals(name)) { javaType = resolveClass(value); builder.javaType(javaType); } else if ("jdbcType".equals(name)) { builder.jdbcType(resolveJdbcType(value)); } else if ("mode".equals(name)) { builder.mode(resolveParameterMode(value)); } else if ("numericScale".equals(name)) { builder.numericScale(Integer.valueOf(value)); } else if ("resultMap".equals(name)) { builder.resultMapId(value); } else if ("typeHandler".equals(name)) { typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) { builder.jdbcTypeName(value); } else if ("property".equals(name)) { // Do Nothing } else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES); } } // <3.2> 如果 TypeHandler 型別處理器的別名非空 if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } // <3.3> 建立 ParameterMapping 物件 return builder.build(); } private Map<String, String> parseParameterMapping(String content) { try { return new ParameterExpression(content); } catch (BuilderException ex) { throw ex; } catch (Exception ex) { throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); } } }構造方法:建立additionalParameters
對應的MetaObject物件,便於操作上下文的引數集合,包含附加引數集合(通過 <bind />
標籤生成的,或者<foreach />
標籤中的集合的元素)
handleToken(String content)
方法:
-
呼叫
buildParameterMapping(content)
方法,解析#{}
的內容建立ParameterMapping
物件 -
直接返回
?
佔位符
buildParameterMapping(content)
方法:
- 將字串解析成 key-value 鍵值對,通過
org.apache.ibatis.builder.ParameterExpression
進行解析,其中有一個key為"property",value就是對應的屬性名稱 - 獲得屬性的名字和型別
- 建立
ParameterMapping.Builder
構建者物件,設定引數的名稱與Java Type- 將上面第
1
步解析到key-value鍵值對設定到Builder中 - 如果TypeHandler型別處理器的別名非空,則嘗試獲取其對應的型別處理器並設定到Builder中
- 通過Builder建立
ParameterMapping
物件,如果沒有配置TypeHandler型別處理器,則根據引數Java Type和Jdbc Type從TypeHandlerRegistry
註冊中心獲取並賦值到該物件中
- 將上面第
ParameterExpression
org.apache.ibatis.builder.ParameterExpression
:繼承了HashMap<String, String>
,引數表示式處理器,在ParameterMappingTokenHandler
處理#{}
的內容時需要通過其解析成key-value鍵值對
構造方法:
public class ParameterExpression extends HashMap<String, String> { private static final long serialVersionUID = -2417552199605158680L; /** * 從類的註釋中可以看出我們可以這樣定義佔位符 * 1. #{propertyName, javaType=string, jdbcType=VARCHAR} * 2. #{(expression), javaType=string, jdbcType=VARCHAR} * * @param expression 我們定義的佔位符表示式 */ public ParameterExpression(String expression) { parse(expression); } }在建構函式中呼叫其parse(String expression)
方法
先出去前面的空格或者非法字元,然後呼叫property(String expression, int left)
方法
如果left
開始位置小於字串的長度,那麼開始解析
-
呼叫
skipUntil
方法,獲取從left
開始,
或者:
第一個位置,也就是分隔符的位置 -
這裡第一次進入的話就會先獲取第一個
,
的位置,那麼呼叫trimmedStr
方法擷取前面的字串,也就是屬性名稱,然後存放一個鍵值對(key為property,value為屬性名稱)
-
呼叫
jdbcTypeOpt(String expression, int p)
方法,繼續解析後面的字串,也就是該屬性的相關配置
如果p
(第一個,
的位置)後面還有字串
則呼叫option(String expression, int p)
方法將一個,
後面的字串解析成key-value鍵值對儲存
逐步解析,將字串解析成key-value鍵值對儲存,這裡儲存的都是屬性的相關配置,例如JdbcType
配置
ParameterMapping
org.apache.ibatis.mapping.ParameterMapping
:儲存#{}
中配置的屬性引數資訊,一個普通的實體類,程式碼如下:
SqlSource
org.apache.ibatis.mapping.SqlSource
:SQL 資源介面,用於建立BoundSql物件(包含可執行的SQL語句與引數資訊),程式碼如下:
StaticSqlSource
org.apache.ibatis.builder.StaticSqlSource
:實現 SqlSource 介面,靜態的 SqlSource 實現類,程式碼如下:
在SqlSourceBuilder
構建的SqlSource型別就是StaticSqlSource
,用於獲取最終的靜態 SQL 語句
RawSqlSource
org.apache.ibatis.scripting.defaults.RawSqlSource
:實現了SqlSource介面,靜態SQL語句對應的SqlSource物件,用於建立靜態 SQL 資源,程式碼如下:
在建構函式中我們可以看到,會先呼叫getSql
方法直接建立SqlSource
因為靜態的 SQL 語句,不需要根據入參來進行邏輯上的判斷處理,所以這裡在建構函式中就先初始化好 SqlSource,後續需要呼叫Mapper介面執行SQL的時候就減少了一定的時間
getSql
方法:
- 建立一個上下文物件
DynamicContext
,入參資訊為null - 呼叫
StaticTextSqlNode
的apply
方法,將所有的SQL拼接在一起 - 返回拼接好的SQL語句
構造方法:
- 建立SqlSourceBuilder構建物件
sqlSourceParser
- 呼叫
sqlSourceParser
的parse
方法對該SQL語句進行轉換,#{}
全部替換成?
佔位符,並建立對應的ParameterMapping
物件 - 第
2
步返回的StaticSqlSource
物件設定到自己的sqlSource
屬性中
getBoundSql
方法:直接通過StaticSqlSource
建立BoundSql
物件
DynamicSqlSource
org.apache.ibatis.scripting.defaults.DynamicSqlSource
:實現了SqlSource介面,動態SQL語句對應的SqlSource物件,用於建立靜態 SQL 資源,程式碼如下:
在建構函式中僅僅是賦值,不像RawSqlSource
的建構函式一樣直接可建立對應的SqlSource物件,因為動態SQL語句需要根據入參資訊,來解析SqlNode節點,所以這裡在getBoundSql
方法中每次都會建立StaticSqlSource
物件
getBoundSql
方法:
- 建立本次解析的動態 SQL 語句的上下文,設定入參資訊
- 根據上下文應用整個 SqlNode,內部包含的所有SqlNode都會被應用,最終解析後的SQL會儲存上下文中
- 建立 SqlSourceBuilder 構建物件
sqlSourceParser
- 呼叫
sqlSourceParser
的parse
方法對第2
步解析後的SQL語句進行轉換,#{}
全部替換成?
佔位符,並建立對應的ParameterMapping
物件 - 通過第
4
步返回的StaticSqlSource
物件建立BoundSql
物件 - 新增附加引數到
BoundSql
物件中,因為上一步建立的BoundSql
物件時候傳入的僅是入參資訊,沒有新增附加引數(通過<bind />
標籤生成的,或者<foreach />
標籤中的集合的元素)
BoundSql
org.apache.ibatis.mapping.BoundSql
:用於資料庫可執行的SQL語句的最終封裝物件,一個普通的實體類,程式碼如下:
DefaultParameterHandler
org.apache.ibatis.scripting.defaults.DefaultParameterHandler
:實現了ParameterHandler介面,預設實現類,僅提供這個實現類,用於將入參設定到java.sql.PreparedStatement
預編譯物件中
回看到org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
語言驅動類中,實現了createParameterHandler
方法,返回的引數處理器就是該物件
程式碼如下:
public class DefaultParameterHandler implements ParameterHandler { private final TypeHandlerRegistry typeHandlerRegistry; /** * MappedStatement 物件 */ private final MappedStatement mappedStatement; /** * 入參 */ private final Object parameterObject; /** * BoundSql 物件,實際的 SQL 語句 */ private final BoundSql boundSql; /** * 全域性配置物件 */ private final Configuration configuration; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); this.parameterObject = parameterObject; this.boundSql = boundSql; } @Override public Object getParameterObject() { return parameterObject; } @Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // 獲取 SQL 的引數資訊 ParameterMapping 物件 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { // 遍歷所有引數 for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); /* * OUT 表示引數僅作為出參,非 OUT 也就是需要作為入參 */ if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; // 獲取入參的屬性名 String propertyName = parameterMapping.getProperty(); /* * 獲取入參的實際值 */ if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params // 在附加引數集合(<bind />標籤生成的)中獲取 value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { // 入參為 null 則該屬性也定義為 null value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { // 有型別處理器,則直接獲取入參物件 value = parameterObject; } else { // 建立入參對應的 MetaObject 物件並獲取該屬性的值 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 獲取定義的引數型別處理器 TypeHandler typeHandler = parameterMapping.getTypeHandler(); // 獲取定義的 Jdbc Type JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { // 如果沒有則設定成 'OTHER' jdbcType = configuration.getJdbcTypeForNull(); } try { // 通過定義的 TypeHandler 引數型別處理器將 value 設定到對應的佔位符 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } } }往PreparedStatement
中設定引數的大致邏輯如下:
- 獲取SQL的引數資訊
ParameterMapping
物件的集合,然後對其遍歷 - 如果引數的模式不為
ParameterMode.OUT
(預設為ParameterMode.IN
),也就是說需要作為入參,那麼開始接下來的賦值 - 獲取該引數對應的屬性名稱,並通過其獲取到對應的值
- 獲取到
TypeHandler
型別處理器(在ParameterMapping
構建的時候會建立對應的TypeHandler
) - 獲取到Jdbc Type
- 通過
TypeHandler
型別處理器,根據引數位置和Jdbc Type將屬性值設定到PreparedStatement
中
這樣就完成對PreparedStatement
的賦值,然後通過它執行SQL語句
總結
在MyBatis初始化的過程中,會將XML對映檔案中的<select /> <insert /> <update /> <delete />
節點解析成MappedStatement
物件,其中會將節點中定義的SQL語句通過XMLLanguageDriver
語言驅動類建立一個SqlSource
物件,本文就是對該物件進行分析
通過SqlSource
這個物件根據入參可以獲取到對應的BoundSql
物件,BoundSql
物件中包含了資料庫需要執行的SQL語句、ParameterMapping
引數資訊、入參物件和附加的引數(通過<bind />
標籤生成的,或者<foreach />
標籤中的集合的元素等等)
好了,對於MyBatis的整個初始化過程我們已經全部分析完了,其中肯定有不對或者迷惑的地方,歡迎指正!!!感謝大家的閱讀!!!