目录[-]html
相信不少人可能都遇到过下面这些异常: java
"Parameter 'xxx' not found. Available parameters are [...]" git
"Could not get property 'xxx' from xxxClass. Cause: github
"The expression 'xxx' evaluated to a null value." sql
"Error evaluating expression 'xxx'. Return value (xxxxx) was not iterable." express
不仅是上面提到的这几个,我认为有不少的错误都产生在和参数有关的地方。 数组
想要避免参数引发的错误,咱们须要深刻了解参数。 mybatis
想了解参数,咱们首先看MyBatis处理参数和使用参数的所有过程。 app
本篇因为为了便于理解和深刻,使用了大量的源码,所以篇幅较长,须要必定的耐心看完,本文必定会对你起到很大的帮助。 ide
在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另外一种是经过SqlSession调用命名空间。这两种方式在传递参数时是不同的,命名空间的方式更直接,可是多个参数时须要咱们本身建立Map做为入参。相比而言,使用接口形式更简单。
接口形式的参数是由MyBatis本身处理的。若是使用接口调用,入参须要通过额外的步骤处理入参,以后就和命名空间方式同样了。
在MapperMethod.java会首先通过下面方法来转换参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
Object convertArgsToSqlCommandParam(Object[] args) {
final
int
paramCount = params.size();
if
(args ==
null
|| paramCount ==
0
) {
return
null
;
}
else
if
(!hasNamedParameters && paramCount ==
1
) {
return
args[params.keySet().iterator().next()];
}
else
{
final
Map<String, Object> param =
new
ParamMap<Object>();
int
i =
0
;
for
(Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final
String genericParamName =
"param"
+ String.valueOf(i +
1
);
if
(!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return
param;
}
}
|
在这里有个很关键的params,这个参数类型为Map<Integer, String>,他会根据接口方法按顺序记录下接口参数的定义的名字,若是使用@Param 指定了名字,就会记录这个名字,若是没有记录,那么就会使用它的序号做为名字。
例若有以下接口:
1
|
List<User> select(
@Param
(
'sex'
)String sex,Integer age);<span></span>
|
那么他对应的params以下:
1
2
3
4
|
{
0:
'sex'
,
1:
'1'
}
|
继续看上面的convertArgsToSqlCommandParam方法,这里简要说明3种状况:
注意:从第3种状况来看,建议各位有多个入参的时候经过@Param 指定参数名,方便后面(动态sql)的使用。
通过上面方法的处理后,在MapperMethod中会继续往下调用命名空间方式的方法:
1
2
|
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.<E>selectList(command.getName(), param);
|
从这以后开始按照统一的方式继续处理入参。
不论是selectOne仍是selectMap方法,归根结底都是经过selectList进行查询的,不论是delete仍是insert方法,都是经过update方法操做的。在selectList和update中全部参数的都进行了统一的处理。
在DefaultSqlSession.java中的wrapCollection方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
Object wrapCollection(
final
Object object) {
if
(object
instanceof
Collection) {
StrictMap<Object> map =
new
StrictMap<Object>();
map.put(
"collection"
, object);
if
(object
instanceof
List) {
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;
}
|
这里特别须要注意的一个地方是map.put("collection", object),这个设计是为了支持Set类型,须要等到MyBatis 3.3.0版本才能使用。
wrapCollection处理的是只有一个参数时,集合和数组的类型转换成Map2类型,而且有默认的Key,从这里你能大概看到为何<foreach>中默认状况下写的array和list(Map类型没有默认值map)。
参数的使用分为两部分:
下面对这两种进行详细讲解,为了方便理解,先讲解第二种状况。
关于动态SQL的基础内容能够查看官方文档。
动态SQL为何会处理参数呢?
主要是由于动态SQL中的<if>,<bind>,<foreache>都会用到表达式,表达式中会用到属性名,属性名对应的属性值如何获取呢?获取方式就在这关键的一步。不知道多少人遇到Could not get property xxx from xxxClass或: Parameter ‘xxx’ not found. Available parameters are[…],都是不懂这里引发的。
在DynamicContext.java中,从构造方法看起:
1
2
3
4
5
6
7
8
9
10
|
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());
}
|
这里的Object parameterObject就是咱们通过前面两步处理后的参数。这个参数通过前面两步处理后,到这里的时候,他只有下面三种状况:
看上面构造方法,若是参数是1,2状况时,执行代码bindings = new ContextMap(null);参数是3状况时执行if中的代码。咱们看看ContextMap类,这是一个内部静态类,代码以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static
class
ContextMap
extends
HashMap<String, Object> {
private
MetaObject parameterMetaObject;
public
ContextMap(MetaObject parameterMetaObject) {
this
.parameterMetaObject = parameterMetaObject;
}
public
Object get(Object key) {
String strKey = (String) key;
if
(
super
.containsKey(strKey)) {
return
super
.get(strKey);
}
if
(parameterMetaObject !=
null
) {
// issue #61 do not modify the context when reading
return
parameterMetaObject.getValue(strKey);
}
return
null
;
}
}
|
咱们先继续看DynamicContext的构造方法,在if/else以后还有两行:
1
2
|
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
|
其中两个Key分别为:
1
2
|
public
static
final
String PARAMETER_OBJECT_KEY =
"_parameter"
;
public
static
final
String DATABASE_ID_KEY =
"_databaseId"
;
|
也就是说1,2两种状况的时候,参数值只存在于"_parameter"的键值中。3状况的时候,参数值存在于"_parameter"的键值中,也存在于bindings自己。
当动态SQL取值的时候会经过OGNL从bindings中获取值。MyBatis在OGNL中注册了ContextMap:
1
2
3
|
static
{
OgnlRuntime.setPropertyAccessor(ContextMap.
class
,
new
ContextAccessor());
}
|
当从ContextMap取值的时候,会执行ContextAccessor中的以下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Override
public
Object getProperty(Map context, Object target, Object name)
throws
OgnlException {
Map map = (Map) target;
Object result = map.get(name);
if
(map.containsKey(name) || result !=
null
) {
return
result;
}
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if
(parameterObject
instanceof
Map) {
return
((Map)parameterObject).get(name);
}
return
null
;
}
|
参数中的target就是ContextMap类型的,因此能够直接强转为Map类型。
参数中的name就是咱们写在动态SQL中的属性名。
下面举例说明这三种状况:
null的时候:
无论name是什么(name="_databaseId"除外,可能会有值),此时Object result = map.get(name);获得的result=null。
在Object parameterObject = map.get(PARAMETER_OBJECT_KEY);中parameterObject=null,所以最后返回的结果是null。
在这种状况下,无论写什么样的属性,值都会是null,而且无论属性是否存在,都不会出错。
Map类型:
此时Object result = map.get(name);通常也不会有值,由于参数值只存在于"_parameter"的键值中。
而后到Object parameterObject = map.get(PARAMETER_OBJECT_KEY);,此时获取到咱们的参数值。
在从参数值((Map)parameterObject).get(name)根据name来获取属性值。
在这一步的时候,若是name属性不存在,就会报错:
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
name属性是什么呢,有什么可选值呢?这就是处理接口形式的入参和处理集合处理后所拥有的Key。
若是你遇到过相似异常,相信看到这儿就明白缘由了。
数组、集合和Map之外的Object类型:
这种类型通过了下面的处理:
MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject);
MetaObject是MyBatis的一个反射类,能够很方便的经过getValue方法获取对象的各类属性(支持集合数组和Map,能够多级属性点.访问,如user.username,user.roles[1].rolename)。
如今分析这种状况。
首先经过name获取属性时Object result = map.get(name);,根据上面ContextMap类中的get方法:
public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject != null) { return parameterMetaObject.getValue(strKey); } return null; }
能够看到这里会优先从Map中取该属性的值,若是不存在,那么必定会执行到下面这行代码:
return parameterMetaObject.getValue(strKey)
若是name恰好是对象的一个属性值,那么经过MetaObject反射能够获取该属性值。若是该对象不包含name属性的值,就会报错:
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);
理解这三种状况后,使用动态SQL应该不会有参数名方面的问题了。
SQL中的两种形式#{username}或者${username},虽然看着差很少,可是实际处理过程差异很大,并且很容易出现莫名其妙的错误。
${username}的使用方式为OGNL方式获取值,和上面的动态SQL同样,这里先说这种状况。
在TextSqlNode.java中有一个内部的静态类BindingTokenParser,如今只看其中的handleToken方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
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);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value ==
null
?
""
: String.valueOf(value));
// issue #274 return "" instead of "null"
checkInjection(srtValue);
return
srtValue;
}
|
从put("value"这个地方能够看出来,MyBatis会建立一个默认为"value"的值,也就是说,在xml中的SQL中能够直接使用${value},从else if能够看出来,只有是简单类型的时候,才会有值。
关于这点,举个简单例子,若是接口为List<User> selectOrderby(String column),若是xml内容为:
<select id="selectOrderby" resultType="User"> select * from user order by ${value} </select>
这种状况下,虽然没有指定一个value属性,可是MyBatis会自动把参数column赋值进去。
再往下的代码:
Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = (value == null ? "" : String.valueOf(value));
这里和动态SQL就同样了,经过OGNL方式来获取值。
看到这里使用OGNL这种方式时,你有没有别的想法?
特殊用法:你是否在SQL查询中使用过某些固定的码值?一旦码值改变的时候须要改动不少地方,可是你又不想把码值做为参数传进来,怎么解决呢?你可能已经明白了。
就是经过OGNL的方式,例若有以下一个码值类:
1
2
3
4
5
|
package
com.abel533.mybatis;
public
interface
Code{
public
static
final
String ENABLE =
"1"
;
public
static
final
String DISABLE =
"0"
;
}
|
若是在xml,能够这么使用:
1
2
3
|
<
select
id
=
"selectUser"
resultType
=
"User"
>
select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE}
</
select
>
|
除了码值以外,你可使用OGNL支持的各类方法,如调用静态方法。
这种方式比较简单,复杂属性的时候使用的MyBatis的MetaObject。
在DefaultParameterHandler.java中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
void
setParameters(PreparedStatement ps)
throws
SQLException {
ErrorContext.instance().activity(
"setting parameters"
).object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if
(parameterMappings !=
null
) {
for
(
int
i =
0
; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if
(parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if
(boundSql.hasAdditionalParameter(propertyName)) {
// issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
}
else
if
(parameterObject ==
null
) {
value =
null
;
}
else
if
(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
else
{
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if
(value ==
null
&& jdbcType ==
null
) {
jdbcType = configuration.getJdbcTypeForNull();
}
typeHandler.setParameter(ps, i +
1
, value, jdbcType);
}
}
}
}
|
上面这段代码就是从参数中取#{propertyName}值的方法,这段代码的主要逻辑就是if/else判断的地方,单独拿出来分析:
1
2
3
4
5
6
7
8
9
10
|
if
(boundSql.hasAdditionalParameter(propertyName)) {
// issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
}
else
if
(parameterObject ==
null
) {
value =
null
;
}
else
if
(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
else
{
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
|
下面咱们说明上面四种状况下的参数名注意事项。
动态参数,这里的参数名和值都由MyBatis动态生成的,所以咱们无法直接接触,也不须要管这儿的命名。可是咱们能够了解一下这儿的命名规则,当之后错误信息看到的时候,咱们能够肯定出错的地方。
在ForEachSqlNode.java中:
private static String itemizeItem(String item, int i) { return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString(); }
其中ITEM_PRFIX为public static final String ITEM_PREFIX = "__frch_";。
若是在<foreach>中的collection="userList" item="user",那么对userList循环产生的动态参数名就是:
__frch_user_0,__frch_user_1,__frch_user_2…
若是访问动态参数的属性,如user.username会被处理成__frch_user_0.username,这种参数值的处理过程在更早以前解析SQL的时候就已经获取了对应的参数值。具体内容看下面有关<foreach>的详细内容。
参数为null,因为这里的判断和参数名无关,所以入参null的时候,在xml中写的#{name}无论name写什么,都不会出错,值都是null。
能够直接使用typeHandler处理的类型。最多见的就是基本类型,例若有这样一个接口方法User selectById(@Param("id")Integer id),在xml中使用id的时候,咱们能够随便使用属性名,无论用什么样的属性名,值都是id。
复杂对象或者Map类型通常都是咱们须要注意的地方,这种状况下,就必须保证入参包含这些属性,若是没有就会报错。这一点和能够参考上面有关MetaObject的地方。
全部动态SQL类型中,<foreach>彷佛是遇到问题最多的一个。
例若有下面的方法:
1
2
3
4
5
6
7
|
<
insert
id
=
"insertUserList"
>
INSERT INTO user(username,password)
VALUES
<
foreach
collection
=
"userList"
item
=
"user"
separator
=
","
>
(#{user.username},#{user.password})
</
foreach
>
</
insert
>
|
对应的接口:
int insertUserList(@Param("userList")List<User> list);
咱们经过foreach源码,看看MyBatis如何处理上面这个例子。
在ForEachSqlNode.java中的apply方法中的前两行:
1
2
|
Map<String, Object> bindings = context.getBindings();
final
Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
|
这里的bindings参数熟悉吗?上面提到过不少。通过一系列的参数处理后,这儿的bindings以下:
1
2
3
4
5
6
7
|
{
"_parameter"
:{
"param1"
:list,
"userList"
:list
},
"_databaseId"
:
null
,
}
|
collectionExpression就是collection="userList"的值userList。
咱们看看evaluator.evaluateIterable如何处理这个参数,在ExpressionEvaluator.java中的evaluateIterable方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
Iterable<?> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if
(value ==
null
) {
throw
new
BuilderException(
"The expression '"
+ expression +
"' evaluated to a null value."
);
}
if
(value
instanceof
Iterable) {
return
(Iterable<?>) value;
}
if
(value.getClass().isArray()) {
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;
}
if
(value
instanceof
Map) {
return
((Map) value).entrySet();
}
throw
new
BuilderException(
"Error evaluating expression '"
+ expression +
"'. Return value ("
+ value +
") was not iterable."
);
}
|
首先经过看第一行代码:
1
|
Object value = OgnlCache.getValue(expression, parameterObject);
|
这里经过OGNL获取到了userList的值。获取userList值的时候可能出现异常,具体能够参考上面动态SQL部分的内容。
userList的值分四种状况。
value == null,这种状况直接抛出异常BuilderException。
value instanceof Iterable,实现Iterable接口的直接返回,如Collection的全部子类,一般是List。
value.getClass().isArray()数组的状况,这种状况会转换为List返回。
value instanceof Map若是是Map,经过((Map) value).entrySet()返回一个Set类型的参数。
经过上面处理后,返回的值,是一个Iterable类型的值,这个值可使用for (Object o : iterable)这种形式循环。
在ForEachSqlNode中对iterable循环的时候,有一段须要关注的代码:
1
2
3
4
5
6
7
8
9
|
if
(o
instanceof
Map.Entry) {
@SuppressWarnings
(
"unchecked"
)
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
}
else
{
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
|
若是是经过((Map) value).entrySet()返回的Set,那么循环取得的子元素都是Map.Entry类型,这个时候会将mapEntry.getKey()存储到index中,mapEntry.getValue()存储到item中。
若是是List,那么会将序号i存到index中,mapEntry.getValue()存储到item中。
这篇文章很长,写这篇文章耗费的时间也很长,超过10小时,写到半夜两点都没写完。
这篇文章真的很是有用,若是你对Mybatis有必定的了解,这篇文章几乎是必读的一篇。
若是各位发现文中错误或者其余问题欢迎留言或加群详谈。