Mybatis源码分析之参数处理
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 (0)];
} else {
Map<String, Object> param = new MapperParamMap<Object>();
for (int i = 0; i < paramCount; i++) {
param.(i), (i)]);
}
/
/ issue #71, add param names as param1, but ensure backward compatibility
for (int i = 0; i < paramCount; i++) {
String genericParamName = "param" + String.valueOf(i + 1);
if (!ainsKey(genericParamName)) {
param.put(genericParamName, (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 && Class().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}
这个时候会将其他两种类型(list或array)也转换为map集合,MapperParamMap和StrictMap都继承了HashMap,只是将ainsKey(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 = Value(expression, parameterObject);
if (value == null) throw new SqlMapperException("The expression '" + expression + "' evaluated to a null value.");
if (value instanceof Iterable) return (Iterable<?>) value;
if (Class().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209).  Do the work manually
// Curse primitives! :) (JGB)
int size = Length(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = (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 = Value(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 = Value(expression, parameterObject);
实际处理
在DynamicSqlSource的getBoundSql⽅法中:
参数绑定
DynamicContext context = new DynamicContext(configuration, parameterObject);
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = wMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, DatabaseId());
}
Node逐级处理(各种标签和${}的处理)
rootSqlNode.apply(context);
这个就是处理动态sql的关键,将if、choose和foreach等剥离出来,使⽤ognl的表达式来获取相关属性的值,例如上⾯提到的foreach和if标签。然后将其转换成简单的text,在TextSqlNode中最终处理${param},将其替换为实际参数值。
替换⽅式如下:
public String handleToken(String content) {
try {
Object parameter = Bindings().get("_parameter");
if (parameter == null) {
} else if (SimpleTypeRegistry.Class())) {
}
Object value = Value(content, Bindings());
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.Sql(), 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, ParameterMappings());
}
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, ParameterMappings());
这个就跟我们直接使⽤JDBC⼀样,使⽤?作为占位符。
最终在DefaultParameterHandler中给设置进参数:
public void setParameters(PreparedStatement ps)
throws SQLException {
ErrorContext.instance().activity("setting parameters").ParameterMap().getId());
List<ParameterMapping> parameterMappings = ParameterMappings();
if (parameterMappings != null) {
MetaObject metaObject = parameterObject == null ? null : wMetaObject(parameterObject);
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = (i);
if (Mode() != ParameterMode.OUT) {
Object value;
String propertyName = Property();
parse error怎么解决
PropertyTokenizer prop = new PropertyTokenizer(propertyName);
if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.Class())) {
value = parameterObject;
} else if (boundSql.hasAdditionalParameter(propertyName)) {
value = AdditionalParameter(propertyName);
} else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
&& boundSql.Name())) {
value = Name());
if (value != null) {
value = wMetaObject(value).getValue(propertyName.Name().length()));
}
} else {
value = metaObject == null ? null : Value(propertyName);
}
TypeHandler typeHandler = TypeHandler();
if (typeHandler == null) {
throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + Id());          }
JdbcType jdbcType = JdbcType();
if (value == null && jdbcType == null) jdbcType = JdbcTypeForNull();
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 = wMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, DatabaseId());
}
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 (ainsKey(strKey)) {
(strKey);
}
if (parameterMetaObject != null) {
Object object = Value(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 = Name());
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 '" + Name() + "' from " + Class() + ".  Cause: " + t.toString(), t);
}
}
这个excludeInner本来就是⼀个boolean类型的参数,哪有什么get⽅法,能调到才怪!
针对上⾯两个实例的分析就结束了,从这⾥也⼤致知道了Mybatis是如何处理参数的。总的来说,不管⼀个参数还是⼏个参数,加@param注解是没错的!加了就会给你统统放map⾥,然后到CoxtMap中取整个map,由于是map类型,将继续到map⾥取具体的对象。
从这⾥可以看出来,如果我们在接⼝中声明时就只⽤⼀个map来装所有参数,key为参数名,value为参数值,然后不使⽤注解,效果也是⼀样的。
有问题欢迎讨论,可以留⾔也可以加本⼈QQ: 646653132

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。