MyBatis 學(xué)習(xí)筆記(八)---源碼分析篇--SQL 執(zhí)行過程詳細分析
前言
在面試中我們經(jīng)常會被到MyBatis中 #{} 占位符與${}
占位符的區(qū)別。大多數(shù)的小伙伴都可以脫口而出#{} 會對值進行轉(zhuǎn)義,防止SQL注入。而${}則會原樣輸出傳入值,不會對傳入值做任何處理。本文將通過源碼層面分析為啥#{} 可以防止SQL注入。
源碼解析
首先我們來看看MyBatis 中SQL的解析過程,MyBatis 會將映射文件中的SQL拆分成一個個SQL分片段,然后在將這些分片段拼接起來。
例如:在映射文件中有如下SQL
SELECT * FROM student
<where>
<if test="id!=null">
id=${id}
</if>
<if test="name!=null">
AND name =${name}
</if>
</where>
MyBatis 會將該SQL 拆分成如下幾部分進行解析
第一部分 SELECT * FROM Author
由StaticTextSqlNode存儲
第二部分 <where>
由WhereSqlNode 存儲
第三部分 <if></if>
由IfSqlNode存儲
第四部分 ${id} ${name}
占位符里的文本由TextSqlNode存儲。
獲取BoundSql
BoundSql 是用來存儲一個完整的SQL 語句,存儲參數(shù)映射列表以及運行時參數(shù)
public class BoundSql {
/**
* 一個完整的SQL語句,可能會包含問號?占位符
*/
private String sql;
/**
* 參數(shù)映射列表,SQL中的每個#{xxx}
* 占位符都會被解析成相應(yīng)的ParameterMapping對象
*/
private List<ParameterMapping> parameterMappings;
/**
* 運行時參數(shù),即用戶傳入的參數(shù),比如Article對象,
* 或是其他的參數(shù)
*/
private Object parameterObject;
/**
* 附加參數(shù)集合,用戶存儲一些額外的信息,比如databaseId等
*/
private Map<String, Object> additionalParameters;
/**
* additionalParameters的元信息對象
*/
private MetaObject metaParameters;
.... 省略部分代碼
}
分析SQL的解析,首先從獲取BoundSql說起。其代碼源頭在MappedStatement。
public BoundSql getBoundSql(Object parameterObject) {
//其實就是調(diào)用sqlSource.getBoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//剩下的可以暫時忽略,故省略代碼
return boundSql;
}
如上,可以看出其內(nèi)部就是調(diào)用的sqlSource.getBoundSql。 而我們sqlSource 接口又有如下幾個實現(xiàn)類。
DynamicSqlSource
RawSqlSource
StaticSqlSource
ProviderSqlSource
VelocitySqlSource
其中DynamicSqlSource 是對動態(tài)SQL進行解析,當(dāng)SQL配置中包含${}
或者<if>
,<set>
等標(biāo)簽時,會被認(rèn)定為是動態(tài)SQL,此時使用 DynamicSqlSource 存儲 SQL 片段,而RawSqlSource 是對原始的SQL 進行解析,而StaticSqlSource 是對靜態(tài)SQL進行解析。這里我們重點介紹下DynamicSqlSource。話不多說,直接看源碼。
public BoundSql getBoundSql(Object parameterObject) {
//生成一個動態(tài)上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
//這里SqlNode.apply只是將${}這種參數(shù)替換掉,并沒有替換#{}這種參數(shù)
rootSqlNode.apply(context);
//調(diào)用SqlSourceBuilder
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//SqlSourceBuilder.parse,注意這里返回的是StaticSqlSource,解析完了就把那些參數(shù)都替換成?了,也就是最基本的JDBC的SQL寫法
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//看似是又去遞歸調(diào)用SqlSource.getBoundSql,其實因為是StaticSqlSource,所以沒問題,不是遞歸調(diào)用
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 將DynamicContext的ContextMap中的內(nèi)容拷貝到BoundSql中
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
如上,該方法主要有如下幾個過程:
- 生成一個動態(tài)上下文
- 解析SQL片段,替換${}類型的參數(shù)
- 解析SQL語句,并將參數(shù)都替換成?
- 調(diào)用StaticSqlSource的getBoundSql獲取BoundSql
- 將DynamicContext的ContextMap中的內(nèi)容拷貝到BoundSql中。
下面通過兩個單元測試用例理解下。
@Test
public void shouldMapNullStringsToNotEmptyStrings() {
final String expected = "id=${id}";
final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
String sql = source.getBoundSql(new Bean("12")).getSql();
Assert.assertEquals("id=12", sql);
}
@Test
public void shouldMapNullStringsToJINHAOEmptyStrings() {
final String expected = "id=#{id}";
final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
String sql = source.getBoundSql(new Bean("12")).getSql();
Assert.assertEquals("id=?", sql);
}
如上,${} 占位符經(jīng)過DynamicSqlSource的getBoundSql 方法之后直接替換成立用戶傳入值,而#{} 占位符則僅僅只是只會被替換成?號,不會被設(shè)值。
DynamicContext
DynamicContext 是SQL語句的上下文,每個SQL片段解析完成之后會存入DynamicContext中。讓我們來看看DynamicContext的相關(guān)代碼。
public DynamicContext(Configuration configuration, Object parameterObject) {
//絕大多數(shù)調(diào)用的地方parameterObject為null
if (parameterObject != null && !(parameterObject instanceof Map)) {
//如果不是map型
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
//存儲額外信息,如databaseId
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
如上,在DynamicContext的構(gòu)造函數(shù)中,根據(jù)傳入的參數(shù)對象是否是Map類型,有兩個不同構(gòu)造ContextMap的方式,而ContextMap作為一個繼承了HashMap的對象,作用就是用于統(tǒng)一參數(shù)的訪問方式:用Map接口方法來訪問數(shù)據(jù)。具體磊說,當(dāng)傳入的參數(shù)對象不是Map類型時,MyBatis會將傳入的POJO對象用MetaObject 對象來封裝,當(dāng)動態(tài)計算sql過程需要獲取數(shù)據(jù)時,用Map 接口的get方法包裝 MetaObject對象的取值過程。
static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
//先去map里找
if (super.containsKey(strKey)) {
return super.get(strKey);
}
//如果沒找到,再用ognl表達式去取值
//如person[0].birthdate.year
if (parameterMetaObject != null) {
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
DynamicContext 的解析到此完成。
解析SQL片段
正如前面所說,一個包含了${}
, <if>
,<where>
等標(biāo)簽的SQL 會被分成很多SQL片段。由SqlNode 的子類進行存儲。
如圖所示,
- StaticTextSqlNode 用于存儲靜態(tài)文本
- TextSqlNode用于存儲帶有${}占位符的文本
- ifSqlNode則用于存儲
<if>
節(jié)點的內(nèi)容 - WhereSqlNode 用于增加WHERE 前綴,然后替換掉AND 和OR 等前綴。
- MixedSqlNode內(nèi)部維護了一個SqlNode集合,用于存儲各種
各樣的SqlNode。
首先我們來看看,MixedSqlNode 的很合SQL節(jié)點。
public class MixedSqlNode implements SqlNode {
//組合模式,擁有一個SqlNode的List
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
//依次調(diào)用list里每個元素的apply
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
如上,從構(gòu)造函數(shù)中可以看出,MixedSqlNode 擁有一個SqlNode的集合。這里利用了組合模式。在此處我重點介紹下TextSqlNode 文本SQL節(jié)點 和IfSqlNode if SQL節(jié)點。
//***TextSqlNode
public boolean apply(DynamicContext context) {
// 創(chuàng)建${} 占位符解析器
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 解析${} 占位符,并將解析結(jié)果添加到DynamicContext中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
// 創(chuàng)建占位符解析器,GenericTokenParser 是一個通用解析器,并非只能解析${}
return new GenericTokenParser("${", "}", handler);
}
在TextSqlNode 類的內(nèi)部持有了一個綁定記號解析器BindingTokenParser,用于解析標(biāo)記內(nèi)容,并將結(jié)果返回給GenericTokenParser。核心代碼如下:
//***TextSqlNode.BindingTokenParser
public String handleToken(String content) {
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);
}
//從緩存里取得值
// 通過ONGL從用戶傳入的參數(shù)中獲取結(jié)果
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
// 通過正則表達式檢測setValue有效性
checkInjection(srtValue);
return srtValue;
}
而GenericTokenParser 則是一個通用的記號解析器,用戶處理#{}
和${}
參數(shù)。核心代碼如下:
//*GenericTokenParser
public String parse(String text) {
StringBuilder builder = new StringBuilder();
if (text != null && text.length() > 0) {
char[] src = text.toCharArray();
int offset = 0;
int start = text.indexOf(openToken, offset);
//#{favouriteSection,jdbcType=VARCHAR}
//這里是循環(huán)解析參數(shù),參考GenericTokenParserTest,比如可以解析${first_name} ${initial} ${last_name} reporting.這樣的字符串,里面有3個 ${}
while (start > -1) {
//判斷一下 ${ 前面是否是反斜杠,這個邏輯在老版的mybatis中(如3.1.0)是沒有的
if (start > 0 && src[start - 1] == '\\') {
// the variable is escaped. remove the backslash.
//新版已經(jīng)沒有調(diào)用substring了,改為調(diào)用如下的offset方式,提高了效率
//issue #760
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
int end = text.indexOf(closeToken, start);
if (end == -1) {
builder.append(src, offset, src.length - offset);
offset = src.length;
} else {
builder.append(src, offset, start - offset);
offset = start + openToken.length();
String content = new String(src, offset, end - offset);
//得到一對大括號里的字符串后,調(diào)用handler.handleToken,比如替換變量這種功能
builder.append(handler.handleToken(content));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
}
return builder.toString();
}
接著我們來看看IfSqlNode,該Sql節(jié)點主要是判斷<if></if>
的條件是否成立,成立的話則調(diào)用其他節(jié)點的apply方法進行解析,并返回true。不成立的話則直接返回false。
public boolean apply(DynamicContext context) {
//通過ONGL評估test 表達式的結(jié)果
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 若test表達式中的條件成立,則調(diào)用其它節(jié)點的apply方法進行解析。
contents.apply(context);
return true;
}
return false;
}
解析#{}占位符
經(jīng)過前面的解析,我們已經(jīng)能夠從DynamicContext 中獲取到完整的SQL語句了。但是這并不意味著解析工作就結(jié)束了。我們還有#{}
占位符沒有處理。#{}
占位符不同于${}
占位符的處理方式。MyBatis 并不會直接將#{}
占位符替換成相應(yīng)的參數(shù)值。
#{}
的解析過程封裝在SqlSourceBuilder 的parse方法中。解析后的結(jié)果交給StaticSqlSource處理。話不多說,來看看源碼吧。
//*SqlSourceBuilder
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 創(chuàng)建#{} 占位符處理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//替換#{}中間的部分,如何替換,邏輯在ParameterMappingTokenHandler
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析#{}占位符,并返回解析結(jié)果
String sql = parser.parse(originalSql);
//封裝解析結(jié)果到StaticSqlSource中,并返回
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
如上源碼,該解析過程主要有四部,核心步驟就是解析#{}
占位符,并返回結(jié)果。GenericTokenParser 類在前面已經(jīng)解析過了,下面我們重點看看SqlSourceBuilder的內(nèi)部類ParameterMappingTokenHandler。該類的核心方法是handleToken方法。該方法的主要作用是將#{}
替換成?
并返回。然后就是構(gòu)建參數(shù)映射。ParameterMappingTokenHandler 該類同樣實現(xiàn)了TokenHandler 接口,所以GenericTokenParser 類的parse方法可以調(diào)用到。
//參數(shù)映射記號處理器,靜態(tài)內(nèi)部類
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
private Class<?> parameterType;
private MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
//獲取context的對應(yīng)的ParameterMapping
parameterMappings.add(buildParameterMapping(content));
//如何替換很簡單,永遠是一個問號,但是參數(shù)的信息要記錄在parameterMappings里面供后續(xù)使用
return "?";
}
//構(gòu)建參數(shù)映射
private ParameterMapping buildParameterMapping(String content) {
//#{favouriteSection,jdbcType=VARCHAR}
//先解析參數(shù)映射,就是轉(zhuǎn)化成一個hashmap
/*
* parseParameterMapping 內(nèi)部依賴 ParameterExpression 對字符串進行解析,ParameterExpression 的
*/
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
// metaParameters 為 DynamicContext 成員變量 bindings 的元信息對象
if (metaParameters.hasGetter(property)) {
/*
* parameterType 是運行時參數(shù)的類型。如果用戶傳入的是單個參數(shù),比如 Article 對象,此時
* parameterType 為 Article.class。如果用戶傳入的多個參數(shù),比如 [id = 1, author = "coolblog"],
* MyBatis 會使用 ParamMap 封裝這些參數(shù),此時 parameterType 為 ParamMap.class。如果
* parameterType 有相應(yīng)的 TypeHandler,這里則把 parameterType 設(shè)為 propertyType
*/
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property != null) {
MetaClass metaClass = MetaClass.forClass(parameterType);
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
// 如果 property 為空,或 parameterType 是 Map 類型,則將 propertyType 設(shè)為 Object.class
propertyType = Object.class;
}
} else {
propertyType = Object.class;
}
// ----------------------------分割線---------------------------------
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
// 將propertyType賦值給javaType
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
// 遍歷propertiesMap
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
// 如果用戶明確配置了javaType,則以用戶的配置為準(zhǔn)。
javaType = resolveClass(value);
builder.javaType(javaType);
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties);
}
}
//#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
如上,buildParameterMapping方法,主要做了如下三件事
- 解析content,
- 解析propertyType,對應(yīng)分割線上面的代碼
- 構(gòu)建ParameterMapping,對應(yīng)分割下下面的代碼。
最終的結(jié)果是將 #{xxx} 占位符中的內(nèi)容解析成 Map。
例如:
上面占位符中的內(nèi)容最終會被解析成如下的結(jié)果:
{
"property": "age",
"typeHandler": "MyTypeHandler",
"jdbcType": "NUMERIC",
"javaType": "int"
}
BoundSql的創(chuàng)建過程就此結(jié)束了。我們接著往下看。
創(chuàng)建StatementHandler
StatementHandler 是非常核心的接口,從代碼分詞的角度來說,StatementHandler是MyBatis源碼的邊界,再往下層就是JDBC層面的接口了。StatementHandler需要和JDBC層面的接口打交道。它要做的事情有很多,在執(zhí)行SQL之前,StatementHandler 需要創(chuàng)建合適的Statement對象。然后填充參數(shù)值到Statement對象中,最后通過Statement 對象執(zhí)行SQL。待SQL執(zhí)行完畢,還需要去處理查詢結(jié)果。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0cMuXOsM-1582632260584)(./images/1559481523011.png)]
設(shè)置運行時參數(shù)到SQL中
JDBC 提供了三種 Statement 接口,分別是 Statement、PreparedStatement 和 CallableStatement。他們的關(guān)系如下:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-25GvS01Y-1582632260586)(./images/1559481899968.png)]
上面三個接口的層級分明,其中 Statement 接口提供了執(zhí)行 SQL,獲取執(zhí)行結(jié)果等基本功能。PreparedStatement 在此基礎(chǔ)上,對 IN 類型的參數(shù)提供了支持。使得我們可以使用運行時參數(shù)替換 SQL 中的問號 ? 占位符,而不用手動拼接 SQL。CallableStatement 則是 在 PreparedStatement 基礎(chǔ)上,對 OUT 類型的參數(shù)提供了支持,該種類型的參數(shù)用于保存存儲過程輸出的結(jié)果。
本節(jié),我將分析 PreparedStatement 的創(chuàng)建,以及設(shè)置運行時參數(shù)到 SQL 中的過程。其他兩種 Statement 的處理過程,大家請自行分析。Statement 的創(chuàng)建入口是在 SimpleExecutor 的 prepareStatement 方法中,下面從這個方法開始進行分析。
//*SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲取數(shù)據(jù)庫連接
Connection connection = getConnection(statementLog);
//創(chuàng)建Statement
stmt = handler.prepare(connection);
//為Statement設(shè)置IN參數(shù)
handler.parameterize(stmt);
return stmt;
}
如上,上面代碼的邏輯不復(fù)雜,總共包含三個步驟。如下:
獲取數(shù)據(jù)庫連接
創(chuàng)建 Statement
為 Statement 設(shè)置 IN 參數(shù)
上面三個步驟看起來并不難實現(xiàn),實際上如果大家愿意寫,也能寫出來。不過 MyBatis 對著三個步驟進行拓展,實現(xiàn)上也相對復(fù)雜一下。以獲取數(shù)據(jù)庫連接為例,MyBatis 并未沒有在 getConnection 方法中直接調(diào)用 JDBC DriverManager 的 getConnection 方法獲取獲取連接,而是通過數(shù)據(jù)源獲取獲取連接。MyBatis 提供了兩種基于 JDBC 接口的數(shù)據(jù)源,分別為 PooledDataSource 和 UnpooledDataSource。創(chuàng)建或獲取數(shù)據(jù)庫連接的操作最終是由這兩個數(shù)據(jù)源執(zhí)行。限于篇幅問題,本節(jié)不打算分析以上兩種數(shù)據(jù)源的源碼,相關(guān)分析會在下一篇文章中展開。
接下來,我將分析 PreparedStatement 的創(chuàng)建,以及 IN 參數(shù)設(shè)置的過程。按照順序,先來分析 PreparedStatement 的創(chuàng)建過程。如下:
//*PreparedStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
Statement statement = null;
try {
// 創(chuàng)建 Statement
statement = instantiateStatement(connection);
// 設(shè)置超時和 FetchSize
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
protected Statement instantiateStatement(Connection connection) throws SQLException {
//調(diào)用Connection.prepareStatement
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}
如上,PreparedStatement 的創(chuàng)建過程沒什么復(fù)雜的地方,就不多說了。下面分析運行時參數(shù)是如何被設(shè)置到 SQL 中的過程。
public void parameterize(Statement statement) throws SQLException {
//通過參數(shù)處理器ParameterHandler設(shè)置運行時參數(shù)到PreparedStatement中
parameterHandler.setParameters((PreparedStatement) statement);
}
地方
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
/*
* 從BoundSql中獲取ParameterMapping列表,每個ParameterMapping
* 與原始SQL中的#{xxx} 占位符一一對應(yīng)
* */
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//循環(huán)設(shè)參數(shù)
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 檢測參數(shù)類型,排除掉mode為OUT類型的parameterMapping
if (parameterMapping.getMode() != ParameterMode.OUT) {
//如果不是OUT,才設(shè)進去
Object value;
// 獲取屬性名
String propertyName = parameterMapping.getProperty();
// 檢測BoundSql的additionalParameter是否包含propertyName
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
//若有額外的參數(shù), 設(shè)為額外的參數(shù)
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
//若參數(shù)為null,直接設(shè)null
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
//若參數(shù)有相應(yīng)的TypeHandler,直接設(shè)object
value = parameterObject;
} else {
//除此以外,MetaObject.getValue反射取得值設(shè)進去
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 之上,獲取#{xxx}占位符屬性所對應(yīng)的運行時參數(shù)
// -------------------分割線-----------------------
// 之下,獲取#{xxx}占位符屬性對應(yīng)的TypeHandler,并在最后通過TypeHandler將運行時參數(shù)值設(shè)置到
// PreparedStatement中。
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
//不同類型的set方法不同,所以委派給子類的setParameter方法
jdbcType = configuration.getJdbcTypeForNull();
}
// 由類型處理器typeHandler向ParameterHandler設(shè)置參數(shù)
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
如上代碼,分割線以上的大段代碼用于獲取 #{xxx} 占位符屬性所對應(yīng)的運行時參數(shù)。分割線以下的代碼則是獲取 #{xxx} 占位符屬性對應(yīng)的 TypeHandler,并在最后通過 TypeHandler 將運行時參數(shù)值設(shè)置到 PreparedStatement 中。關(guān)于 TypeHandler 的用途。
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥