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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 1. 出现的异常以信息代码如下:Mapper接口中的代码:List queryTransCdByType(String type);MapperXML中的SQL代码:SELECT * FROM 表名 t where 1=1 and t.

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

img_5eb37f9bca2d1fe372fcfe66f2d6e678.png
无@Param注解

如果是如下的传参形式:

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

那么它的names的属性是

img_cf08dec284670a266612443de3090182.png
有@Param注解

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

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

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

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

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

    • 举例如果是List<String> queryTransCdByType(@Param("type") String type);,那么Map的组成如下
    img_3cc663b1e9c6ae81e5888e09369d28fe.png
    @Param注解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参数是我们一开始处理的参数,有一下的三种情况

  1. null
  2. Object类型
  3. 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";

此时可以知道在bindingsMap中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. 总结

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

4. 参考文章

相关文章
|
Dubbo Java 应用服务中间件
项目中引进这玩意,排查日志又快又准
随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候,因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难。
|
前端开发
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(下)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
JavaScript 搜索推荐 Java
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(上)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
3月前
|
XML Java Android开发
探索Android开发之旅:打造你的第一个应用
【9月更文挑战第4天】在这篇专为初学者设计的文章中,我们将一起踏上激动人心的Android开发之旅。从设置开发环境到实现一个简单的“Hello World”应用,每一步都充满了发现和学习。文章将引导你理解Android开发的基础知识,并鼓励你动手实践。让我们开始吧,创造你的第一款Android应用,开启技术世界的新篇章!
|
4月前
|
JSON Java fastjson
Java日志通关(五) - 最佳实践
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第五篇。
《C++避坑神器·二十三》C++异常处理exception
《C++避坑神器·二十三》C++异常处理exception
80 0
|
数据库 C++
《C++避坑神器·十七》找到程序崩溃Bug的一个实用方法:dump调试
《C++避坑神器·十七》找到程序崩溃Bug的一个实用方法:dump调试
149 0
|
程序员 数据安全/隐私保护 索引
【python基础知识】11.如何debug -常见报错原因及排查思路 - 思维篇
【python基础知识】11.如何debug -常见报错原因及排查思路 - 思维篇
366 0
|
JavaScript 前端开发 API
每个 Bug 都值得认真对待:分享一个 debug 的案例,推荐给前端实习生参考
每个 Bug 都值得认真对待:分享一个 debug 的案例,推荐给前端实习生参考
310 0

相关实验场景

更多
下一篇
DataWorks