Mybatis 源碼分析(2)—— 參數處理

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裏,然後到ContextMap中取整個map,由於是map類型,將繼續到map裏取具體的對象。

從這裏可以看出來,如果我們在接口中聲明時就只用一個map來裝所有參數,key爲參數名,value爲參數值,然後不使用註解,效果也是一樣的。


有問題歡迎討論,可以留言也可以加本人QQ: 646653132

關於參數綁定的詳細解讀:http://blog.csdn.net/isea533/article/details/44002219

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章