不学无数 — 记一次常见异常而导致的Debug源码之旅

栏目: 数据库 · 发布时间: 7年前

内容简介:代码如下:Mapper接口中的代码:MapperXML中的SQL代码:

代码如下:

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 的属性是

不学无数 — 记一次常见异常而导致的Debug源码之旅

如果是如下的传参形式:

List<String> queryTransCdByType(@Param("type") String type);

复制代码

那么它的 names 的属性是

不学无数 — 记一次常见异常而导致的Debug源码之旅

hasParamAnnotation 参数表示是是否在传参中使用的 @Param 注解

继续看上面的 getNamedParams 方法,有以下的三种情况

  1. 当传入的空参数的时候,那么返回的是null

  2. 如果没有 @Param 注解的时候,那么直接返回的就是所传的参数的值

  3. 如果有 @Param ,注解的话,那么返回的是Map

    • 举例如果是 List<String> queryTransCdByType(@Param("type") String type); ,那么Map的组成如下
    不学无数 — 记一次常见异常而导致的Debug源码之旅
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 参数是我们一开始处理的参数,有一下的三种情况

@Param

如果是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;
}
}

复制代码

此时有三种情况。

parameterMetaObject
parameterMetaObject

此时如果是返回了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. 总结

其实这个问题如果不深究的话也很好解决,直接粘贴错误信息到百度上直接查找就会知道是怎么错了。但是还是想知道为什么会报这个错,在深究这个错误期间,也学习了很多,里面运用了许多的设计模式,也借着这个机会学会了代理模式,和组合模式。第一次写关于源码解析的文章,借鉴了许多大佬的东西,也加入了自己的见解。如有不足,请多指出。

4. 参考文章


以上所述就是小编给大家介绍的《不学无数 — 记一次常见异常而导致的Debug源码之旅》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming in Haskell

Programming in Haskell

Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99

Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具