美文网首页Java技术分享
不学无数——记一次常见异常而导致的Debug源码之旅

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

作者: 不学无数的程序员 | 来源:发表于2018-09-12 16:41 被阅读11次

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

    无@Param注解

    如果是如下的传参形式:

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

    那么它的names的属性是

    有@Param注解

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

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

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

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

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

      • 举例如果是List<String> queryTransCdByType(@Param("type") String type);,那么Map的组成如下
      @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. 参考文章

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/pmfbgftx.html