Mybatis參數設置過程分為參數解析處理、動態SQL轉為靜態SQL、預編譯參數賦值幾個步驟,其中動態SQL轉為靜態SQL的過程中會涉及預編譯`${}`參數賦值和預編譯參數的解析工作。本文將從一個簡單的示例出發了解Mybatis參數設置的全部流程,文章會涉及一些常見的問題:
* `${}`占位符的處理流程;
* `#{}`占位符的處理流程;
* ParameterHandler#setParameters的執行流程;
### 從Demo開始
這個demo是在原來的基礎上進行了改造,不過也僅僅是mapper.xml部分,大家先有個簡單的印象,方便后續文章進行分析。
* CompanyMapper.xml
xml中僅有一個select查詢語句,為了便于分析`${}`與`#{}`兩種占位符的差別,我在一個語句中分別使用了兩種不同的方式來設置參數id。
~~~xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.raysonxin.dao.CompanyDao">
<resultMap id="baseResultMap" type="com.raysonxin.dataobject.CompanyDO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="cpy_type" property="cpyType"/>
</resultMap>
<select id="selectById" resultMap="baseResultMap">
select id,name,cpy_type from company
<where>
<if test="id != null">
id = ${id} or id = #{id}
</if>
</where>
</select>
</mapper>
復制代碼
~~~
* CompanyDao.java
~~~java
public interface CompanyDao {
/**
* 根據id查詢CompanyDO
*
* @param cmpId 主鍵id
* @return 查詢結果
*/
CompanyDO selectById(@Param("id") Integer cmpId);
}
復制代碼
~~~
* Program.java
~~~java
public class Program {
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 獲取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 創建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
CompanyDO companyDO = dao.selectById(1);
System.out.println(companyDO);
}
}
}
復制代碼
~~~
下圖來自之前的文章[《Mybatis源碼之SQL執行過程》](https://mp.weixin.qq.com/s/XPxWxvpffQCXyELjcNH6lA),經過前文的分析,我們知道Mapper接口的方法是通過動態代理的方式由代理類MapperProxy委托到SqlSession對應的接口執行的,MapperProxy#invoke才是Mapper方法的真正入口,而**本文所要分析的參數設置過程也是從這里開始的**(確切來講是MapperProxy#invoke方法所調用的MapperMethod#execute方法)。
### 
Mybatis的參數設置流程,與我們常規意義上調用方法的參數設置有些不同。因為我們調用Mapper接口的方法,其參數雖然在接口中有定義,但是實際是作用在SQL語句上,而SQL語句是通過占位符(**`${}`與`#{}`**)來預占參數的。
**這里其實就可能存在一定的差異,Mapper接口方法所定義的參數名稱、個數、類型、順序等一定與SQL語句的要求匹配嗎?如果匹配,那Mybatis是如何把我們給定的參數,按照預期為SQL語句設置參數的呢?帶著這幾個問題,我們開始Mybatis參數設置流程的分析!**
### 參數解析
由于Mapper接口方法執行時調用的是MapperMethod,其執行依賴MappedStatement中SqlSource對參數的要求。因此,在方法執行之前,Mybatis需要先把Mapper接口參數名稱解析為通用的、可被理解的方式;同時,引入一些特性支持開發人員的參數定制。
結合示例代碼,我們使用的是select語句,也就是會執行SELECT命令,MapperMethod#execute僅貼出與SELECT有關的代碼進行說明。
~~~java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
//省略INSERT、UPDATE、DELTE、FLUSH……
//僅查看SELECT命令
case SELECT:
//無返回值或者具有ResultHandler
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
}
//返回列表
else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
}
//返回Map
else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
}
//返回Cursor
else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
}
//返回單個結果
else {
//參數轉換:把參數轉為sql命令參數
Object param = method.convertArgsToSqlCommandParam(args);
//調用SqlSession#selectOne方法
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
復制代碼
~~~
在SELECT命令最后一個else的代碼塊(第27行),調用了MethodSignature#convertArgsToSqlCommandParam方法,作用是把我們通過Mapper接口傳入的參數轉為SQL命令參數。先通過斷點看下方法執行后的效果:  示例中我們給接口selectById傳入的參數為id=1(但是參數名id其實是不存在的),經過這個方法的處理,我們得到了一個名為param的ParamMap對象,其中包含兩個鍵值對:id=1,param1=1;接著就把param交給了SqlSession#selectOne方法去執行。先不要著急想為什么這么轉換,我們先來了解下轉換的過程(我已經貼心的為繪制了這一過程的時序圖)。  跟著代碼走,我們會一步步走到ParamNameResolver#getNamedParams,在此之前已經在MapperMethod構造方法中完成了ParamNameResolver的實例化,所以我們先看下ParamNameResolver的構造方法,然后再了解getNamedParams的實現過程。(***同一個類,代碼放在一起了,省略部分無關方法***)。
~~~java
public class ParamNameResolver {
//通用參數名稱前綴
private static final String GENERIC_NAME_PREFIX = "param";
/**
* <p>
* The key is the index and the value is the name of the parameter.
* The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
* the parameter index is used. Note that this index could be different from the actual index
* when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
* </p>
* <ul>
* <li>aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}</li>
* <li>aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}</li>
* <li>aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}</li>
* </ul>
*/
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
// 獲取方法的參數類型,示例中即Integer
final Class<?>[] paramTypes = method.getParameterTypes();
// 獲取方法參數的@Param注解列表
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
// 從@Param中獲取參數名稱
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 是否為特殊參數,特殊參數是指RowBounds、ResultHandler
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
// 按照參數順序獲取注解
for (Annotation annotation : paramAnnotations[paramIndex]) {
//判斷是否為@Param注解
if (annotation instanceof Param) {
// 標記具有@Param注解
hasParamAnnotation = true;
// 獲取@Param注解中的參數名稱,示例中“cmpId”
name = ((Param) annotation).value();
// 獲取到注解中的參數即停止
break;
}
}
// 到這里,說明從注解中未獲取到參數名稱
if (name == null) {
// @Param was not specified.
// 未定指定@Param,允許使用實際參數名稱,我們示例中的id
if (config.isUseActualParamName()) {
// 獲取方法中實際參數名稱
name = getActualParamName(method, paramIndex);
}
// 走到這,就采用參數的索引順序作為名稱,如“0”、“1”。。。
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
// 以參數索引順序為key、上述過程獲取到的名稱為value,存儲到map
// 示例中:0->id
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
//……
/**
* <p>
* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
// args為null,或者paramCount=0,返回null
if (args == null || paramCount == 0) {
return null;
}
// 無@Param注解并且只有一個參數,直接返回args的第一個元素
else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
}
// 使用@Param注解或有多個參數
else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
// 遍歷names
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 以names的value為key,以args值為value,添加到param
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// 添加通用參數,如param1、param2.。。
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
// 防止重復,覆蓋已有參數
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}
復制代碼
~~~
ParamNameResolver構造方法完成了參數名稱的獲取,其中會跳過特殊類型參數,如RowBounds、ResultHandler,最終以k-v方式存儲至SortedMap。參數名稱獲取過程遵循的優先級規則是:
* 首先,獲取@Param注解中設置的參數名稱,如示例中的id;
* 其次,使用接口方法定義中的參數名稱,如示例中的cmpId;
* 最后,使用參數索引編號作為名稱,如“0”、“1”……
getNamedParams方法是為了構造參數名稱與參數值的映射關系,\*\*首先\*\*\*\*把構造方法中獲取到的參數名稱與參數值存儲到map中,然后為增加通用參數名稱與參數值的關系。\*\*這樣,在接口方法簽名中或@Param注解中的參數名稱不與通用參數發生沖突的情況下,每個參數值都會對應兩個key,即上面調試截圖的形式。
雖然占用的篇幅較多,但是參數解析的過程還是比較簡單的,Mybatis結合全局配置,通過反射技術、@Param注解等方式,把接口入參轉為一個Map結構,下一步調用SqlSession相關接口完成執行。
### 動態SQL轉為靜態SQL【可選】
*這里的動態SQL是指DynamicSqlSource,靜態SQL是指StaticSqlSource。我對這兩者的理解是:*
* *DynamicSqlSource:SQL語句存在動態元素,需要在運行時通過參數值才能將其轉換為結構確定的、可以直接賦值的SQL語句,也就是靜態化的SQL;*
* *StaticSqlSource:包含形如`select * from tb_test where id=?`這樣的SQL以及參數映射列表,只需要把其中的`?`替換為指定參數即可執行。*
通過[《Mybatis源碼之MappedStatement》](https://mp.weixin.qq.com/s/lbOfdQsiAcrLv1YJsP_vEw)這篇文章,我們可以知道,如果在SQL語句中使用了動態元素(如:if標簽、${}占位符等)MappedStatement#SqlSource類型為DynamicSqlSource,否則為RawSqlSource。其中,RawSqlSource內部其實封裝了一個StaticSqlSource,已經完成了靜態化處理及SQL語句中參數的提取操作。**示例中,我們的SQL語句使用where、if這類標簽,顯然是動態SQL。所以,我們接下來按照動態SQL的流程來分析。**
#### 主體流程
“參數解析”一節,代碼調用到了SqlSession#selectOne方法,隨著代碼執行,我們可以走到一個關鍵的方法MappedStatement#getBoundSql,它用來獲取一個BoundSql對象。先看一下這段代碼的執行邏輯:
~~~java
public BoundSql getBoundSql(Object parameterObject) {
// 通過SqlSource獲取BoundSql對象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 獲取BoundSql中的參數映射列表
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//如果parameterMappings為空,則使用xml中的參數映射配置。
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// 一般用不到,省略了
return boundSql;
}
復制代碼
~~~
getBoundSql就是為了獲取BoundSql對象,首先通過SqlSource獲取,如果未獲取到parameterMappings,則考慮使用Configuration(即xml配置文件)中的ParameterMap作為參數映射。我們主要分析通過SqlSource獲取的過程:示例中是動態SQL,所以這里的sqlSource是DynamicSqlSource的實例。
~~~java
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
//org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
// 創建動態上下文對象,它里面主要是我們的從第一節獲取到的參數解析結果
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 動態SQL內部的SqlNode是MixedSqlNode實例,由多種不同的SqlNode組成,apply會依次調用SqlNode的apply方法
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 動態sql的轉換過程就在這里。
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 創建BoundSql對象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
復制代碼
~~~
* DynamicContext對象存儲了這個階段所需要的參數信息、提供了解析后sql語句拼接的工具方法及并存儲處理后的sql語句,它貫穿SqlSource解析的整個過程。
* rootSqlNode是MixedSqlNode的實例,它內存通過contents字段存儲了SQL語句所包含的SqlNode列表,通過apply方法依次調用各個SqlNode#apply方法,完成SqlNode中表達式的計算及結果輸出。例如:通過計算表達式的結果,決定標簽內的SQL是否滿足執行條件。由于SqlNode的類型有多種,處理方式也不盡相同,具體類型需要具體分析。
* SqlSourceBuilder顧名思義,就是用來做SqlSource構建的工具類,具體來說是StaticSqlSource的構建類。正是通過它完成了從動態到靜態的處理過程,期間會提取出SQL語句中通過`#{}`占位符預設的參數映射信息。
DynamicSqlSource#getBoundSql方法的執行過程就比較清晰了,整體分為兩個步驟:根據輸入參數把MixedSqlNode中各個子SqlNode靜態化,然后解析SQL語句中的`#{}`占位符所定義的參數映射信息并以`?`替換,創建StaticSqlSource對象和BoundSql。先通過下圖概括這個階段的執行過程,其中黃色注釋部分是需要具體展開的。 
#### MixedSqlNode#apply
MixedSqlNode代碼如下所示,contents存儲SQL所包含的所有SqlNode,apply方法執行時,會依次把MixedSqlNode#contents中的SqlNode遍歷并調用apply方法。
~~~java
// 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) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
復制代碼
~~~
示例中的contents如下圖所示,有兩個StaticTextSqlNode、一個IfSqlNode(它內部存儲的也是一個MixedSqlNode,包含一個TextSqlNode)。  MixedSqlNode#apply方法的分析過程就比較容易理解,只需要依次看下StaticTextSqlNode、IfSqlNode、TextSqlNode對apply方法的實現過程即可。
* StaticTextSqlNode:靜態文本,沒有動態元素和參數處理,僅僅是拼接sql語句;
~~~java
//org.apache.ibatis.scripting.xmltags.StaticTextSqlNode#apply
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
// org.apache.ibatis.scripting.xmltags.DynamicContext#appendSql
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
復制代碼
~~~
* IfSqlNode:涉及ONGL表達式的判斷,當表達式成立時,再調用IfSqlNode中的SqlNode#apply方法。
~~~java
//org.apache.ibatis.scripting.xmltags.IfSqlNode#apply
public boolean apply(DynamicContext context) {
//表達式成立繼續執行。
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
復制代碼
~~~
* 此時,SqlNode的類型是MixedSqlNode,跟上面的邏輯類似,所以會執行到TextSqlNode。
\*\* \*\*TextSqlNode#apply\*\*\*\*雖然代碼只有兩行,\*\***這個過程比較復雜一些,涉及到了占位符`${}`的解析過程,我們重點看下。代碼如下:**
~~~java
// org.apache.ibatis.scripting.xmltags.TextSqlNode#apply
public boolean apply(DynamicContext context) {
// 創建${}占位符的解析器,其中GenericTokenParser是通用的,BindingTokenParser是對占位符的替換方式實現
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//解析${},完成替換,把解析結果拼接到sql語句中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
//創建GenericTokenParser,處理的目標是`${`和`}`開閉包含的內容
return new GenericTokenParser("${", "}", handler);
}
復制代碼
~~~
GenericTokenParser是一個通用的SQL語句占位符的解析類,會從頭到尾依次匹配openToken(如:`${`、`#{`)和closeToken(如:`}`)中間包含的表達式。若匹配到表達式,就會通過TokenHandler接口對象,對匹配表達式進行處理。通過上述代碼可以,當處理`${}`時使用的是BindingTokenParser。
示例中,GenericTokenParser#parse方法的入參text是(如上圖TextSqlNode的內容):
~~~sql
id = ${id} or id = #{id}
復制代碼
~~~
GenericTokenParser的parse方法比較長,但是很簡單,我們重點來看下當匹配到表達式的處理流程,即BindingTokenParser#handleToken方法:
~~~java
private static class BindingTokenParser implements TokenHandler {
// 上下文對象,內容包含了參數及sql語句解析結果
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
// 入參content是GenericTokenParser匹配到的表達式,示例中為“id”
public String handleToken(String content) {
// 獲取輸入的參數對象。示例為ParamMap,包含:"id"->1,"param1"->1
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獲取表達式對應的值,這里獲取的value=1。
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
//sql入侵檢測
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());
}
}
}
復制代碼
~~~
代碼走下來,我們知道BindingTokenParser#handleToken方法是完成了對GenericTokenParser#parse匹配到表達式的替換,即:由`id`替換為參數對應的值1。所以,原來SQL語句前后變化如下:
~~~sql
# GenericTokenParser#parse入參
id = ${id} or id = #{id}
# GenericTokenParser#parse輸出結果
id = 1 or id = #{id}
復制代碼
~~~
以上處理結果會保存在DynamicContext中,MixedSqlNode#apply也就執行完成了。MixedSqlNode#contents中的SqlNode列表也被處理為了一條比較簡潔的SQL語句,如下:
~~~sql
select id,name,cpy_type from company WHERE id = 1 or id = #{id}
復制代碼
~~~
#### SqlSourceBuilder#parse
剛剛有說到,SqlSourceBuilder#parse的作用是匹配SQL語句中的`#{}`占位符,并將匹配到的表達式解析為參數映射列表。我們通過代碼了解下過程:
~~~java
//org.apache.ibatis.builder.SqlSourceBuilder#parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
復制代碼
~~~
相信大家一眼就看出來了,SqlSourceBuilder#parse的處理過程與TextSqlNode#apply的過程如出一轍,區別就在于SqlSourceBuilder#parse處理的是`#{}`,而且使用了ParameterMappingTokenHandler這個TokenHandler實現類。所以,我們只需要了解ParameterMappingTokenHandler#handleToken處理過程即可:
~~~java
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
復制代碼
~~~
handleToken通過buildParameterMapping方法創建了ParameterMapping(參數映射)并添加到parameterMappings參數映射列表中;同時返回了`?`用于替換原來GenericTokenParser匹配到的表達式。也就是我們常說的,`#{}`內的參數會被`?`替換。
**buildParameterMapping方法比較長,也比較復雜,我現在也沒有完全理解,其中的case也沒有遇到過,等以后理解了再來補充**。雖然未完全理解,但不影響我們對整個過程的把握。在我們的例子中,它會使用構造器ParameterMapping.Builder把表達式轉為ParameterMapping,其中會尋找參數映射所需的TypeHandler,用于后續的參數處理。
動態SQL轉為靜態SQL這個過程就分析完了,其實主要就是兩件事:一是ONGL通過輸入參數把SqlNode中的SQL片段轉為靜態的SQL,二是解析處理SQL語句中的參數占位符`${}`和`#{}`,Mybatis對二者的處理有很大的不同。對于`${}`直接將表達式替換為對應的參數值,對于`#{}`提取參數信息轉為ParameterMapping,并將原來的表示是替換為`?`。
### 預編譯參數賦值
終于來到最后一個環節!獲得BoundSql之后,回到Mybatis對SQL的執行流程,就會從CachingExecutor一直走到PreparedStatementHandler#parameterize方法,它會使用DefaultParameterHandler對預編譯SQL執行參數賦值,它是ParameterHandler的默認實現。當方法運行到這里后,DefaultParameterHandler的關鍵字段情況如下圖所示:  結合上圖中boundSql等字段的信息,我們看下setParameters方法的執行流程:
~~~java
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 通過BoundSql獲取參數映射列表
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 僅參數映射列表非空時處理
if (parameterMappings != null) {
// 遍歷參數映射列表
for (int i = 0; i < parameterMappings.size(); i++) {
// 獲取當前參數映射
ParameterMapping parameterMapping = parameterMappings.get(i);
// 僅參數類型不是OUT時處理,示例為IN
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 獲取參數名稱
String propertyName = parameterMapping.getProperty();
// 額外參數是否包含當前屬性,當前是_parameter和_databaseId
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
}
// 類型處理器是否包含參數類型,當前參數類型為ParamMap
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
// 創建MetaObject對象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
// 獲取屬性對應的值
value = metaObject.getValue(propertyName);
}
// 獲取參數映射的類型處理器。這里是UnknownTypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
// 獲取jdbcType
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//通過類型處理器為ps參數賦值
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
復制代碼
~~~
setParameters方法的執行過程比較清晰:依次遍歷參數映射列表,從parameterObject中獲取參數對應的值,然后通過類型處理器為PreparedStatement指定索引位置的參數賦值,最終完成所有參數的賦值操作。
這里需要注意的是,在通過TypeHandler為PreparedStatement賦值時,Mybatis會通過類型處理器注冊中心查找與當前參數匹配的TypeHandler。因為我們的ParameterMapping是通過對SQL語句中預編譯占位符`#{}`的解析而得到的,這里并沒有明確參數的確切類型,Mybatis為ParameterMapping默認了UnknownTypeHandler作為處理器,即把參數類型默認為Object。所以,在我們的示例中會首先通過UnknownTypeHandler,然后通過resolveTypeHandler找到正確的類型處理器IntegerTypeHandler為PreparedStatement完成賦值操作。
### 總結
Mybatis參數設置過程到這里就完成了,非常感謝你能耐心的讀到這里!簡單做個總結吧:Mybatis的參數設置過程其實不僅僅是參數設置那么簡單,它涉及了Mybatis對入參的轉換處理,MappedStatement對參數的要求,而且對不同的占位符采取了不同的處理方式,以滿足我們多種情景的要求;最終通過DefaultParameterHandler完成了對預編譯聲明PreparedStatement的參數賦值。
我們的示例是比較簡單的,僅僅包含了一個參數,而且未涉及這種復雜的標簽,你也可以打開源碼去了解下。
作者:碼路印記
鏈接:https://juejin.cn/post/6888957681492492295
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊