1. 出现的异常以信息
代码如下:
Mapper接口中的代码:
List<String> queryTransCdByType(String type);
MapperXML中的SQL代码:
<select id="queryTransCdByType" resultType="String" parameterType="String">
SELECT * FROM 表名 t where 1=1
<if test="type!=''">
and t.type = #{type}
</if>
</select>
单元测试进行测试SQL的执行情况时,报了如下的错误:
org.mybatis.spring.MyBatisSystemException: nested exception is
org.apache.ibatis.reflection.ReflectionException:
There is no getter for property named 'type' in 'class java.lang.String'
报错是在接口中没有加
@Param
参数进行参数的命名,所以在SQL中使用动态SQL时候找不到key=type
的值,所以报了错误。关于不知道什么是动态SQL的可以看动态 SQL官网的介绍
2. 源码分析找错误
于是上网查了许多的资料,关于为什么会报这个错误,进行了如下的整理。
在MyBatis源码中MapperMethod.java
会首先经过下面方法来转换参数:
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (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;
}
}
在这里有个很关键的names
,这个参数类型为Map<Integer, String>
,他会根据接口方法按顺序记录下接口参数的定义的名字,如果使用@Param
指定了名字,那么就会将就会记录这个名字,用Map
进行记录,key是用 GENERIC_NAME_PREFIX + String.valueOf(i + 1);
生成的。value是@Param
指定的名字。
GENERIC_NAME_PREFIX
的定义如下:
private static final String GENERIC_NAME_PREFIX = "param";
例如在接口中如下的传参形式:
List<String> queryTransCdByType(String type);
那么它的names
的属性是
如果是如下的传参形式:
List<String> queryTransCdByType(@Param("type") String type);
那么它的names
的属性是
hasParamAnnotation
参数表示是是否在传参中使用的@Param
注解
继续看上面的getNamedParams
方法,有以下的三种情况
- 当传入的空参数的时候,那么返回的是null
- 如果没有
@Param
注解的时候,那么直接返回的就是所传的参数的值 - 如果有
@Param
,注解的话,那么返回的是Map- 举例如果是
List<String> queryTransCdByType(@Param("type") String type);
,那么Map的组成如下
- 举例如果是
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
//如果参数含有rowBounds则调用分页的查询
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
得到了参数以后就会调用下面这段语句
result = sqlSession.<E>selectList(command.getName(), param);
进入selectList()
方法以后代码如下
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
继续往下debug执行,我们会看到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());
}
其中 Object parameterObject
参数是我们一开始处理的参数,有一下的三种情况
- null
- Object类型
- Map类型:带
@Param
注解会是Map类型
如果是1和2情况的话,那么就会进入
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
此时我们进去configuration.newMetaObject(parameterObject)
里面,会发现最后返回MetaObject
return new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
MetaObject是MyBatis的一个反射类,可以很方便的通过getValue方法获取对象的各种属性(支持集合数组和Map,可以多级属性点.访问
此时我们再往下看,会发现有一个ContextMap
静态内部类,代码如下
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
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;
}
}
可以看到,如果是1和2情况的话,那么this.parameterMetaObject
不是空,如果是3情况的话,那么this.parameterMetaObject
就是Null。
再接着从DynamicContext
类往下看
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
-----
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
此时可以知道在bindings
Map中key为_parameter
的value是之前我们解析出来的参数,不管是Map类型还是Object类型全都在里面。当随后进行解析从ContextMap
中取值的时候会调用ContextAccessor
类下面的方法
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;
}
Object target
就是我们之前封装的bindings
参数。Object name
是经过解析的在xml中if标签中的值,此时name=type
我们会先看到他直接从map中取值,进去以后代码如下
@Override
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;
}
}
此时有三种情况。
- 如果map中直接有key为type的值,那么就直接返回。
- 如果是
parameterMetaObject
不为空的情况,既我们上面说的 1,2情况时,那么就会调用利用反射进行拿值,而正因为我在开头那个问题,此时通过反射想拿type值,但是String中没有type值,所以反射拿值的时候报错 - 如果是第三种情况
parameterMetaObject
是Null的情况,那么就直接返回null
此时如果是返回了null会继续下面的代码
if (map.containsKey(name) || result != null) {
return result;
}
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
因为返回的result是null,所以执行到
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
从map中拿到_parameter
的值。如果他是map的话,那么再从map中取出所解析出来的值。
3. 总结
其实这个问题如果不深究的话也很好解决,直接粘贴错误信息到百度上直接查找就会知道是怎么错了。但是还是想知道为什么会报这个错,在深究这个错误期间,也学习了很多,里面运用了许多的设计模式,也借着这个机会学会了代理模式,和组合模式。第一次写关于源码解析的文章,借鉴了许多大佬的东西,也加入了自己的见解。如有不足,请多指出。