zoukankan      html  css  js  c++  java
  • Mybatis源码分析之参数处理

    Mybatis对参数的处理是值得推敲的,不然在使用的过程中对发生的一系列错误直接懵逼了。

    以前遇到参数绑定相关的错误我就是直接给加@param注解,也稀里糊涂地解决了,但是后来遇到了一些问题推翻了我的假设:单个参数不需要使用 @param 。由此产生了一个疑问,Mybatis到底是怎么处理参数的?

    几种常见的情景:

    • 单个参数
      • 不使用注解,基于${}和#{}的引用,基本类型和自定义对象都可以
      • 不使用注解,基于foreach标签的使用,list和array不可以
      • 不使用注解,基于if标签的判断,基本类型 boolean 也报错

    初步封装

    第一次处理是在MapperMethod中:

    private Object getParam(Object[] args) {
        final int paramCount = paramPositions.size();
        if (args == null || paramCount == 0) {
          return null;
        } else if (!hasNamedParameters && paramCount == 1) {
          return args[paramPositions.get(0)];
        } else {
          Map<String, Object> param = new MapperParamMap<Object>();
          for (int i = 0; i < paramCount; i++) {
            param.put(paramNames.get(i), args[paramPositions.get(i)]);
          }
          // issue #71, add param names as param1, param2...but ensure backward compatibility
          for (int i = 0; i < paramCount; i++) {
            String genericParamName = "param" + String.valueOf(i + 1);
            if (!param.containsKey(genericParamName)) {
              param.put(genericParamName, args[paramPositions.get(i)]);
            }
          }
          return param;
        }
    }

    这里会有三种可能:null,object[],MapperParamMap,第三种可以构造出我们常见的param1、parm2……

    AuthAdminUser findAuthAdminUserByUserId(@Param(“userId”) String userId);

    当我们在Mapper接口中如此定义时,就会走上面的else代码块,MapperParamMap将包含两个元素,一个key为userId,另一个为param1。

    第二次处理是在DefaultSqlSession中,调用executor的query方法时,将参数包装成集合:

      private Object wrapCollection(final Object object) {
        if (object instanceof List) {
          StrictMap<Object> map = new StrictMap<Object>();
          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;
      }

    这个时候会将其他两种类型(list或array)也转换为map集合,MapperParamMap和StrictMap都继承了HashMap,只是将super.containsKey(key)为false的时候抛出了一个异常。

    实例呈现

    当我们写Mapper接口时,一个参数通常也不使用@param注解。

    如果这个参数是 List 类型呢?

    List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);

    对应的mapper配置文件:

    <select id="selectFeeItemTypeNameByIds" parameterType="java.util.List" resultType="java.lang.String">
        SELECT fee_item_type_name
        FROM tb_uhome_fee_item_type
        WHERE fee_item_type_id IN
        <foreach collection="itemIds" item="itemId" open="(" close=")" separator="," >
            #{itemId}
        </foreach>
    </select>

    测试一下,直接报错:

    nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘itemIds’ not found. Available parameters are [list]

    然后把itemIds替换为list就好了:

    <foreach collection="list" item="itemId" open="(" close=")" separator="," >
        #{itemId}
    </foreach>

    这个正是验证了上述源码中的操作,在DefaultSqlSession的wrapCollection方法中:

    if (object instanceof List) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("list", object);
      return map;
    }

    如果这个参数用在 if 标签中呢?

    List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);

    xml中这样使用:

    <select id="selectPayMethodListByPlatform" resultType="java.util.HashMap" parameterType="boolean">
        select a.`NAME`as payMethodName, a.`VALUE` as payMethod
        from tb_fcs_dictionary a
        where a.`CODE` = 'PAY_METHOD'
        and a.`STATUS` = 1
        and a.TYPE = 'PLATFORM'
        <if test="excludeInner">
            and a.value not in (14,98)
        </if>
    </select>

    直接报如下错误:

    There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’

    跟踪下DynamicContext的内部类ContextAccessor的getProperty方法:

    那我们加上注解@Param(“excludeInner”) 再看看:

    没有使用注解,存储的就是一个Boolean类型的值,返回null。使用了注解,这个值有名称且存放在MapperParamMap中,直接可以根据名称取到。

    查看调用栈

    在ForEachSqlNode中会调用ExpressionEvaluator的evaluateIterable方法来获取迭代器对象:

    public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
        try {
          Object value = OgnlCache.getValue(expression, parameterObject);
          if (value == null) throw new SqlMapperException("The expression '" + expression + "' evaluated to a null value.");
          if (value instanceof Iterable) return (Iterable<?>) value;
          if (value.getClass().isArray()) {
              // the array may be primitive, so Arrays.asList() may throw 
              // a ClassCastException (issue 209).  Do the work manually
              // Curse primitives! :) (JGB)
              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;
          }
          throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
        } catch (OgnlException e) {
          throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
        }
    }

    IfSqlNode中也会调用ExpressionEvaluator的evaluateBoolean方法来检测表达式正确与否:

    public boolean evaluateBoolean(String expression, Object parameterObject) {
        try {
          Object value = OgnlCache.getValue(expression, parameterObject);
          if (value instanceof Boolean) return (Boolean) value;
          if (value instanceof Number) return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
          return value != null;
        } catch (OgnlException e) {
          throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
        }
    }

    两者都会使用Ognl来获取表达式的值:

    Object value = OgnlCache.getValue(expression, parameterObject);

    实际处理

    在DynamicSqlSource的getBoundSql方法中:

    • 参数绑定
      DynamicContext context = new DynamicContext(configuration, parameterObject);
      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());
      }
    • Node逐级处理(各种标签和${}的处理)

    rootSqlNode.apply(context);

    这个就是处理动态sql的关键,将if、choose和foreach等剥离出来,使用ognl的表达式来获取相关属性的值,例如上面提到的foreach和if标签。

    然后将其转换成简单的text,在TextSqlNode中最终处理${param},将其替换为实际参数值。

    替换方式如下:

    public String handleToken(String content) {
      try {
        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());
        return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
      } catch (OgnlException e) {
        throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
      }
    }
    • 参数解析(#{}的处理)

    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);

    SqlSourceBuilder#parse:

    public SqlSource parse(String originalSql, Class<?> parameterType) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    GenericTokenParser的parse方法将#{xx}替换为 ? ,如下面的sql语句:

    SELECT DISTINCT
        A.ORGAN_ID as organId,
        CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
    FROM
        ORGAN A,
        ORGAN_REL B,
        V_USER_ORGAN C
    WHERE
        A.ORGAN_ID = B.ORGAN_ID
    AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
    AND B.PAR_ID = 1
    AND A.STATUS = 1
    AND C.USER_ID = #{userId}

    替换后为:

    SELECT DISTINCT
        A.ORGAN_ID as organId,
        CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
    FROM
        ORGAN A,
        ORGAN_REL B,
        V_USER_ORGAN C
    WHERE
        A.ORGAN_ID = B.ORGAN_ID
    AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
    AND B.PAR_ID = 1
    AND A.STATUS = 1
    AND C.USER_ID = ?

    然后构造一个StaticSqlSource:

    new StaticSqlSource(configuration, sql, handler.getParameterMappings());

    这个就跟我们直接使用JDBC一样,使用?作为占位符。

    最终在DefaultParameterHandler中给设置进参数:

    public void setParameters(PreparedStatement ps)
          throws SQLException {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();
              PropertyTokenizer prop = new PropertyTokenizer(propertyName);
              if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } else if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
                  && boundSql.hasAdditionalParameter(prop.getName())) {
                value = boundSql.getAdditionalParameter(prop.getName());
                if (value != null) {
                  value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
                }
              } else {
                value = metaObject == null ? null : metaObject.getValue(propertyName);
              }
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              if (typeHandler == null) {
                throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
              }
              JdbcType jdbcType = parameterMapping.getJdbcType();
              if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
              typeHandler.setParameter(ps, i + 1, value, jdbcType);
            }
          }
        }
      }

    这里分为五种情况(高版本合并了第三和第四种):
    - parameterObject为null,value直接为null
    - parameterObject类型为typeHandlerRegistry中匹配类型value直接赋值为parameterObject
    - 参数是动态参数,通过动态参数取值
    - 参数是动态参数而且是foreach中的(前缀为frch),也是通过动态参数取值
    - 复杂对象或者map类型,通过反射取值

    总结

    像 if 和 foreach 这种标签都是直接通过Ognl来取值。

    “${}” 的处理在TextSqlNode中,使用OGNL方式取值,当场替换为实际参数值。

    “#{}” 的处理在SqlSourceBuilder的parse中,使用占位符(?)替换,最后在设置参数的时候使用Mybatis的MetaObject取值。

    当我们使用单个参数未用注解时:
    - 用在形如foreach和if的标签中(针对上面两个实例)

    List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);
    
    List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);

    MapperMethod的getParam方法将返回这两个参数本身。

    DefaultSqlSession的wrapCollection方法将把list放到一个key为”list”的map中,boolean类型的还是返回本身。

    这样在DynamicSqlSource的getBoundSql方法中构造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());
    }

    list类型的由于被包装了一下,将走else。而boolean类型直接创建一个包含metaObject的ContextMap。

    不管怎样,“itemIds”走到这里已经丢了,后面解析表达式的时候根据这个名字是肯定拿不到的。

    而boolean类型的”excludeInner”将在ContextMap中如此出现(仅仅有个值key却为“_parameter”):

    key: "_parameter"  value: true
    key: "_databaseId"  value: "MySQL"

    不过它持有的MetaObject类型的parameterMetaObject对象却不为null。

    看下ContextMap中的重写的get方法:

    public Object get(Object key) {
          String strKey = (String) key;
          if (super.containsKey(strKey)) {
            return super.get(strKey);
          }
    
          if (parameterMetaObject != null) {
            Object object = parameterMetaObject.getValue(strKey);
            if (object != null) {
              super.put(strKey, object);
            }
    
            return object;
          }
    
          return null;
    }

    当父类中没有时(这个肯定没有),它将去parameterMetaObject中拿,这一拿就拿出问题来了:

    There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’

    一路跟到MetaObject的getValue方法,又到BeanWrapper的get方法,然后就把它当做一个普通的对象,用反射去调它的get方法:

    private Object getBeanProperty(PropertyTokenizer prop, Object object) {
        try {
          Invoker method = metaClass.getGetInvoker(prop.getName());
          try {
            return method.invoke(object, NO_ARGUMENTS);
          } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
          }
        } catch (RuntimeException e) {
          // 进了这个运行时异常:说它没的get方法,哈哈
          throw e;
        } catch (Throwable t) {
          throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ".  Cause: " + t.toString(), t);
        }
    }

    这个excludeInner本来就是一个boolean类型的参数,哪有什么get方法,能调到才怪!

    针对上面两个实例的分析就结束了,从这里也大致知道了Mybatis是如何处理参数的。总的来说,不管一个参数还是几个参数,加@param注解是没错的!加了就会给你统统放map里,然后到CoxtMap中取整个map,由于是map类型,将继续到map里取具体的对象。

    从这里可以看出来,如果我们在接口中声明时就只用一个map来装所有参数,key为参数名,value为参数值,然后不使用注解,效果也是一样的。


    有问题欢迎讨论,可以留言也可以加本人QQ: 646653132

    关于参数绑定的详细解读:http://blog.csdn.net/isea533/article/details/44002219

  • 相关阅读:
    GoJS实例1
    MAVEN 添加本地jar
    找不到xml、找不到类
    office365激活码序列号密钥:
    【转载】EF Core & Castle DynamicProxy基本用法(AOP)
    【转载】SQL Server
    【转载】对象克隆(C# 快速高效率复制对象另一种方式 表达式树转)
    [转载] Emit动态生成代码
    【转载】Windows 下搭建 wmi_exporter+Prometheus+Grafana 服务器性能监控平台
    Java RMI 使用
  • 原文地址:https://www.cnblogs.com/lucare/p/8679128.html
Copyright © 2011-2022 走看看