Mybatis 原始碼分析(2)—— 引數處理
Mybatis對引數的處理是值得推敲的,不然在使用的過程中對發生的一系列錯誤直接懵逼了。
以前遇到引數繫結相關的錯誤我就是直接給加@param註解,也稀裡糊塗地解決了,但是後來遇到了一些問題推翻了我的假設:單個引數不需要使用 @param 。由此產生了一個疑問,Mybatis到底是怎麼處理引數的?
幾種常見的情景:
- 單個引數
-
- 不使用註解,基於${}和#{}的引用,基本型別和自定義物件都可以
-
- 不使用註解,基於foreach標籤的使用,list和array不可以
-
- 不使用註解,基於if標籤的判斷,基本型別 boolean 也報錯
初步封裝
第一次處理是在MapperMethod中:
private Object getParam(Object[] args) {
final int paramCount = paramPositions.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasNamedParameters && paramCount == 1) {
return args[paramPositions.get(0)];
} else {
Map<String, Object> param = new MapperParamMap<Object>();
for (int i = 0; i < paramCount; i++) {
param.put(paramNames.get(i), args[paramPositions.get(i)]);
}
// issue #71, add param names as param1, param2...but ensure backward compatibility
for (int i = 0; i < paramCount; i++) {
String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[paramPositions.get(i)]);
}
}
return param;
}
}
這裡會有三種可能:null,object[],MapperParamMap,第三種可以構造出我們常見的param1、parm2……
AuthAdminUser findAuthAdminUserByUserId(@Param(“userId”) String userId);
當我們在Mapper介面中如此定義時,就會走上面的else程式碼塊,MapperParamMap將包含兩個元素,一個key為userId,另一個為param1。
第二次處理是在DefaultSqlSession中,呼叫executor的query方法時,將引數包裝成集合:
private Object wrapCollection(final Object object) {
if (object instanceof List) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("list", object);
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}
這個時候會將其他兩種型別(list或array)也轉換為map集合,MapperParamMap和StrictMap都繼承了HashMap,只是將super.containsKey(key)為false的時候丟擲了一個異常。
例項呈現
當我們寫Mapper介面時,一個引數通常也不使用@param註解。
如果這個引數是 List 型別呢?
List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);
對應的mapper配置檔案:
<select id="selectFeeItemTypeNameByIds" parameterType="java.util.List" resultType="java.lang.String">
SELECT fee_item_type_name
FROM tb_uhome_fee_item_type
WHERE fee_item_type_id IN
<foreach collection="itemIds" item="itemId" open="(" close=")" separator="," >
#{itemId}
</foreach>
</select>
測試一下,直接報錯:
nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘itemIds’ not found. Available parameters are [list]
然後把itemIds替換為list就好了:
<foreach collection="list" item="itemId" open="(" close=")" separator="," >
#{itemId}
</foreach>
這個正是驗證了上述原始碼中的操作,在DefaultSqlSession的wrapCollection方法中:
if (object instanceof List) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("list", object);
return map;
}
如果這個引數用在 if 標籤中呢?
List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);
xml中這樣使用:
<select id="selectPayMethodListByPlatform" resultType="java.util.HashMap" parameterType="boolean">
select a.`NAME`as payMethodName, a.`VALUE` as payMethod
from tb_fcs_dictionary a
where a.`CODE` = 'PAY_METHOD'
and a.`STATUS` = 1
and a.TYPE = 'PLATFORM'
<if test="excludeInner">
and a.value not in (14,98)
</if>
</select>
直接報如下錯誤:
There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’
跟蹤下DynamicContext的內部類ContextAccessor的getProperty方法:
那我們加上註解@Param(“excludeInner”) 再看看:
沒有使用註解,儲存的就是一個Boolean型別的值,返回null。使用了註解,這個值有名稱且存放在MapperParamMap中,直接可以根據名稱取到。
檢視呼叫棧
在ForEachSqlNode中會呼叫ExpressionEvaluator的evaluateIterable方法來獲取迭代器物件:
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
try {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) throw new SqlMapperException("The expression '" + expression + "' evaluated to a null value.");
if (value instanceof Iterable) return (Iterable<?>) value;
if (value.getClass().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
IfSqlNode中也會呼叫ExpressionEvaluator的evaluateBoolean方法來檢測表示式正確與否:
public boolean evaluateBoolean(String expression, Object parameterObject) {
try {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) return (Boolean) value;
if (value instanceof Number) return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
return value != null;
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
兩者都會使用Ognl來獲取表示式的值:
Object value = OgnlCache.getValue(expression, parameterObject);
實際處理
在DynamicSqlSource的getBoundSql方法中:
- 引數繫結
DynamicContext context = new DynamicContext(configuration, parameterObject);
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
- Node逐級處理(各種標籤和${}的處理)
rootSqlNode.apply(context);
這個就是處理動態sql的關鍵,將if、choose和foreach等剝離出來,使用ognl的表示式來獲取相關屬性的值,例如上面提到的foreach和if標籤。
然後將其轉換成簡單的text,在TextSqlNode中最終處理${param},將其替換為實際引數值。
替換方式如下:
public String handleToken(String content) {
try {
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);
}
Object value = OgnlCache.getValue(content, context.getBindings());
return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
}
}
- 引數解析(#{}的處理)
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
SqlSourceBuilder#parse:
public SqlSource parse(String originalSql, Class<?> parameterType) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
GenericTokenParser的parse方法將#{xx}替換為 ? ,如下面的sql語句:
SELECT DISTINCT
A.ORGAN_ID as organId,
CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
FROM
ORGAN A,
ORGAN_REL B,
V_USER_ORGAN C
WHERE
A.ORGAN_ID = B.ORGAN_ID
AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
AND B.PAR_ID = 1
AND A.STATUS = 1
AND C.USER_ID = #{userId}
替換後為:
SELECT DISTINCT
A.ORGAN_ID as organId,
CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
FROM
ORGAN A,
ORGAN_REL B,
V_USER_ORGAN C
WHERE
A.ORGAN_ID = B.ORGAN_ID
AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
AND B.PAR_ID = 1
AND A.STATUS = 1
AND C.USER_ID = ?
然後構造一個StaticSqlSource:
new StaticSqlSource(configuration, sql, handler.getParameterMappings());
這個就跟我們直接使用JDBC一樣,使用?作為佔位符。
最終在DefaultParameterHandler中給設定進引數:
public void setParameters(PreparedStatement ps)
throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
PropertyTokenizer prop = new PropertyTokenizer(propertyName);
if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
&& boundSql.hasAdditionalParameter(prop.getName())) {
value = boundSql.getAdditionalParameter(prop.getName());
if (value != null) {
value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
}
} else {
value = metaObject == null ? null : metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
if (typeHandler == null) {
throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
}
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
這裡分為五種情況(高版本合併了第三和第四種):
- parameterObject為null,value直接為null
- parameterObject型別為typeHandlerRegistry中匹配型別value直接賦值為parameterObject
- 引數是動態引數,通過動態引數取值
- 引數是動態引數而且是foreach中的(字首為frch),也是通過動態引數取值
- 複雜物件或者map型別,通過反射取值
總結
像 if 和 foreach 這種標籤都是直接通過Ognl來取值。
“${}” 的處理在TextSqlNode中,使用OGNL方式取值,當場替換為實際引數值。
“#{}” 的處理在SqlSourceBuilder的parse中,使用佔位符(?)替換,最後在設定引數的時候使用Mybatis的MetaObject取值。
當我們使用單個引數未用註解時:
- 用在形如foreach和if的標籤中(針對上面兩個例項)
List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);
List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);
MapperMethod的getParam方法將返回這兩個引數本身。
DefaultSqlSession的wrapCollection方法將把list放到一個key為 “list”的map中,boolean型別的還是返回本身。
這樣在DynamicSqlSource的getBoundSql方法中構造DynamicContext時:
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
list型別的由於被包裝了一下,將走else。而boolean型別直接建立一個包含metaObject的ContextMap。
不管怎樣,“itemIds”走到這裡已經丟了,後面解析表示式的時候根據這個名字是肯定拿不到的。
而boolean型別的 “excludeInner” 將在ContextMap中如此出現(僅僅有個值key卻為“_parameter”):
key: "_parameter" value: true
key: "_databaseId" value: "MySQL"
不過它持有的MetaObject型別的parameterMetaObject物件卻不為null。
看下ContextMap中的重寫的get方法:
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
Object object = parameterMetaObject.getValue(strKey);
if (object != null) {
super.put(strKey, object);
}
return object;
}
return null;
}
當父類中沒有時(這個肯定沒有),它將去parameterMetaObject中拿,這一拿就拿出問題來了:
There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’
一路跟到MetaObject的getValue方法,又到BeanWrapper的get方法,然後就把它當做一個普通的物件,用反射去調它的get方法:
private Object getBeanProperty(PropertyTokenizer prop, Object object) {
try {
Invoker method = metaClass.getGetInvoker(prop.getName());
try {
return method.invoke(object, NO_ARGUMENTS);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (RuntimeException e) {
// 進了這個執行時異常:說它沒的get方法,哈哈
throw e;
} catch (Throwable t) {
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);
}
}
這個excludeInner本來就是一個boolean型別的引數,哪有什麼get方法,能調到才怪!
針對上面兩個例項的分析就結束了,從這裡也大致知道了Mybatis是如何處理引數的。總的來說,不管一個引數還是幾個引數,加@param註解是沒錯的!加了就會給你統統放map裡,然後到ContextMap中取整個map,由於是map型別,將繼續到map裡取具體的物件。
從這裡可以看出來,如果我們在介面中宣告時就只用一個map來裝所有引數,key為引數名,value為引數值,然後不使用註解,效果也是一樣的。
有問題歡迎討論,可以留言也可以加本人QQ: 646653132
關於引數繫結的詳細解讀:http://blog.csdn.net/isea533/article/details/44002219