MyBatis parses XML tags and placeholders related source code analysis

beginning

Today, kid X encountered a bug in the development process and raised an ISSUE to mybatis: throw ReflectionException when using #{array.length}

To roughly explain the problem, in mapper.xml, when using #{array.length} to get the length of the array, a ReflectionException will be reported. Code:

public List<QuestionnaireSent> selectByIds(Integer[] ids) { 
    return commonSession.selectList("QuestionnaireSentMapper.selectByIds", ImmutableMap.of("ids", ids)); 
}

The corresponding xml:

<select id="selectByIds">
	SELECT * FROM t_questionnaire
	<if test="ids.length > 0">
		WHERE id in
		<foreach collection="ids" open="(" separator="," close=")" item="id">#{id}
		</foreach>
	</if>
	LIMIT #{ids.length}
</select>

The following is an analysis of the problem with the source code

analyze

There are two uses of length in xml, so which one is the cause of this error?

Try to remove the test condition and keep the limit, but still report an error. Then the locatable error is caused by #{ids.length}.

This leads to two questions:

  1. How conditions are parsed in XML tags (extensions, how foreach parses arrays and collections)
  2. How #{ids.length} is parsed

With these two questions, we enter the source code

Analysis of the first part of XML tags

in class org.apache.ibatis.scripting.xmltags.XMLScriptBuilder

private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
}
protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<SqlNode>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

In each corresponding Handler, there is corresponding processing logic.

Take IfHandler as an example:

private class IfHandler implements NodeHandler {
  public IfHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    String test = nodeToHandle.getStringAttribute("test");
    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
    targetContents.add(ifSqlNode);
  }
}

Here, IfSqlNode is mainly generated and parsed in the corresponding class

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  @Override
  public boolean apply(DynamicContext context) {
    // OGNL执行test语句
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
}

ExpressionEvaluator uses OGNL expressions to evaluate.

Another advanced example: ForEachSqlNode, which includes parsing of arrays, Collections and Maps. The core is to obtain the corresponding iterators through OGNL:

final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);

public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
  Object value = OgnlCache.getValue(expression, parameterObject);
  if (value == null) {
    throw new BuilderException("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>();
      // 数组为何要这样处理?参考后记1
      for (int i = 0; i < size; i++) {
          Object o = Array.get(value, i);
          answer.add(o);
      }
      return answer;
  }
  if (value instanceof Map) {
    return ((Map) value).entrySet();
  }
  throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
}

There is an interesting note in the middle, refer to Postscript 1.

Analysis of the second part ${}, #{}

First need to be clear:

  1. ${}: use OGNL to dynamically execute the content, the result is spelled in SQL
  2. #{}: Parse as a parameter marker, and use the parsed content as a parameter of prepareStatement.

For the xml tag, the expression in it is also the parsing method of ${}, which is parsed using the OGNL expression.

For parameter marker parsing, mybatis uses a parser designed by itself, and uses the reflection mechanism to obtain various attributes.

Take #{bean.property} as an example, use reflection to get the property value of the bean. His analysis process is as follows:

  1. BaseExecutor.createCacheKey方法

In this method, all parameter mappings are traversed and parsed, and the specific value of the parameter is obtained according to the propertyName value in #{propertyName}

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        // 第二步
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        // 第四步
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}
  1. MetaObject metaObject = configuration.newMetaObject(parameterObject);

This step is to obtain the MetaObject object, which is used to wrap the object object according to the object type, so that the value can be obtained later according to the #{propertyName} expression. This includes the process of recursively looking up object properties.

public MetaObject newMetaObject(Object object) {
  return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}
public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
  // 防止后续传入空对象,空对象特殊处理
  if (object == null) {
    return SystemMetaObject.NULL_META_OBJECT;
  } else {
    // 第三步
    return new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
  }
}
  1. new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);

In this step, the MetaObject object is generated, and different objectWrapper objects are generated according to the specific type of the object.

private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
  this.originalObject = object;
  this.objectFactory = objectFactory;
  this.objectWrapperFactory = objectWrapperFactory;
  this.reflectorFactory = reflectorFactory;

  if (object instanceof ObjectWrapper) {
    // 已经是ObjectWrapper对象,则直接返回
    this.objectWrapper = (ObjectWrapper) object;
  } else if (objectWrapperFactory.hasWrapperFor(object)) {
    // 工厂获取obejctWrapper
    this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
  } else if (object instanceof Map) {
    // Map类型的Wrapper,主要用户根据name从map中获取值的封装,具体看源码
    this.objectWrapper = new MapWrapper(this, (Map) object);
  } else if (object instanceof Collection) {
    // collection类的包装器,关于此还有个注意点,参考后记3
    this.objectWrapper = new CollectionWrapper(this, (Collection) object);
  } else if (object.getClass().isArray()) {
    // 数组类型的包装器,这个处理逻辑是发现了一个bug后我自己加的,后面说。
    this.objectWrapper = new ArrayWrapper(this, object);
  } else {
    // 原始bean的包装器,主要通过反射获取属性,以及递归获取属性。
    this.objectWrapper = new BeanWrapper(this, object);
  }
}
  1. value = metaObject.getValue(propertyName);

This step really gets the value represented by #{propertyName}

public Object getValue(String name) {
  // 把propertyName进行Tokenizer化,最简单的例子是用.分割的name,处理为格式化的多级property类型。
  PropertyTokenizer prop = new PropertyTokenizer(name);
  if (prop.hasNext()) {
    // 如果有子级的property即bean.property后面的property,即进入下面的递归过程
    MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
    if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
      return null;
    } else {
      // 开始递归
      return metaValue.getValue(prop.getChildren());
    }
  } else {
    // 第五步:递归终止,直接获取属性。
    return objectWrapper.get(prop);
  }
}
public MetaObject metaObjectForProperty(String name) {
  Object value = getValue(name);
  return MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
}
  1. objectWrapper.get(prop);

The real attribute value is obtained through the objectWrapper generated in the third step. Different wrappers are obtained in different ways. Take beanWrapper as an example:

public Object get(PropertyTokenizer prop) {
  if (prop.getIndex() != null) {
    // 如果有索引即bean[i].property中的[i]时,则尝试解析为collection并取对应的索引值
    Object collection = resolveCollection(prop, object);
    return getCollectionValue(prop, collection);
  } else {
    return getBeanProperty(prop, object);
  }
}

protected Object resolveCollection(PropertyTokenizer prop, Object object) {
  if ("".equals(prop.getName())) {
    return object;
  } else {
    return metaObject.getValue(prop.getName());
  }
}

protected Object getCollectionValue(PropertyTokenizer prop, Object collection) {
  if (collection instanceof Map) {
    // 如果是map,则直接取"i"对应的value
    return ((Map) collection).get(prop.getIndex());
  } else {
    // 否则取集合或者数组中的对应值。下面一堆神奇的if else if是为啥,参考后记2
    int i = Integer.parseInt(prop.getIndex());
    if (collection instanceof List) {
      return ((List) collection).get(i);
    } else if (collection instanceof Object[]) {
      return ((Object[]) collection)[i];
    } else if (collection instanceof char[]) {
      return ((char[]) collection)[i];
    } else if (collection instanceof boolean[]) {
      return ((boolean[]) collection)[i];
    } else if (collection instanceof byte[]) {
      return ((byte[]) collection)[i];
    } else if (collection instanceof double[]) {
      return ((double[]) collection)[i];
    } else if (collection instanceof float[]) {
      return ((float[]) collection)[i];
    } else if (collection instanceof int[]) {
      return ((int[]) collection)[i];
    } else if (collection instanceof long[]) {
      return ((long[]) collection)[i];
    } else if (collection instanceof short[]) {
      return ((short[]) collection)[i];
    } else {
      throw new ReflectionException("The '" + prop.getName() + "' property of " + collection + " is not a List or Array.");
    }
  }
}

private Object getBeanProperty(PropertyTokenizer prop, Object object) {
  try {
    // 反射获取getter方法。
    Invoker method = metaClass.getGetInvoker(prop.getName());
    try {
      // 执行getter方法获取值
      return method.invoke(object, NO_ARGUMENTS);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  } catch (RuntimeException e) {
    throw e;
  } catch (Throwable t) {
    throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ".  Cause: " + t.toString(), t);
  }
}

At this point, the parsing of #{propertyName} is complete. ${} is the OGNL expression parsing used directly, and will not be parsed in detail.

in conclusion

Let's go back to the problem, and after careful analysis, we get the cause of the error:

In the third step above, the generated ObjectWrapper type is BeanWrapper, and to obtain the attribute value length in BeanWrapper, reflection will be called to try to obtain the getter method and execute it. For an object of an array type, it is of course impossible to have a getter method (only refers to java).

There is no problem with ids.length in test, because the expression in test is executed using OGNL. See ExpressionEvaluator in Part 1. The last is the code logic in the second part of the execution, so an error is reported.

solve

There are three solutions:

  1. Replace #{array.length} with ${array.length} to solve.
  2. use <bind />
<bind name="idCount" value="ids.length" />
LIMIT #{idCount}

Readers can try to see the processing logic of the bind tag. 3. As above, add the ArrayWrapper:

public class ArrayWrapper implements ObjectWrapper {

  private final Object object;

  public ArrayWrapper(MetaObject metaObject, Object object) {
    if (object.getClass().isArray()) {
      this.object = object;
    } else {
      throw new IllegalArgumentException("object must be an array");
    }
  }

  @Override
  public Object get(PropertyTokenizer prop) {
    if ("length".equals(prop.getName())) {
      return Array.getLength(object);
    }
    throw new UnsupportedOperationException();
  }
  ... // 其他未覆盖方法均抛出UnsupportedOperationException异常。
}

Here, the length of the array is obtained by judging the attribute value as "length", and all other exceptions are thrown. This supports the acquisition of the length of the array in the #{} placeholder.

postscript

  1. interesting note
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;
}

What do the annotations mean? This means that when using Arrays.asList() to convert an array to a List, a ClassCastException may be thrown. When the array is an array of primitive type, a ClassCastException is bound to be thrown.

For a detailed analysis of the reasons, see the Arrays.asList() method

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

According to the principle of generic elimination, the parameter type actually received here is Object[], and the array type has a special inheritance relationship.

new Integer[]{} instanceof Object[] = true

When the element type 1 of the A array is a subclass of type 2, the A array is an instance of the type 2 array type. That is, when type1 is like type2, type1 array type is a subclass of type2 array type.

But there is a special case. Arrays of some primitive types (int, char...) are not subclasses of any type of array. When casting int[] to Object[], a ClassCastException will be thrown. Although primitive types are automatically boxed when they are received with Object, arrays of primitive types are not automatically boxed. This is the root cause. This is the reason why this comment appears, and the fundamental reason to traverse the array, use Object to take elements and put them into List.

  1. a bunch of if else if branches

The reason is basically the same as above. Each primitive type array type is a special type, so it needs special treatment.

  1. Notes on CollectionWrapper

Look directly at the code:

public class CollectionWrapper implements ObjectWrapper {

  private final Collection<Object> object;

  public CollectionWrapper(MetaObject metaObject, Collection<Object> object) {
    this.object = object;
  }
  public Object get(PropertyTokenizer prop) {
    throw new UnsupportedOperationException();
  }
  public void set(PropertyTokenizer prop, Object value) {
    throw new UnsupportedOperationException();
  }
  public String findProperty(String name, boolean useCamelCaseMapping) {
    throw new UnsupportedOperationException();
  }
  public String[] getGetterNames() {
    throw new UnsupportedOperationException();
  }
  public String[] getSetterNames() {
    throw new UnsupportedOperationException();
  }
  public Class<?> getSetterType(String name) {
    throw new UnsupportedOperationException();
  }
  public Class<?> getGetterType(String name) {
    throw new UnsupportedOperationException();
  }
  public boolean hasSetter(String name) {
    throw new UnsupportedOperationException();
  }
  public boolean hasGetter(String name) {
    throw new UnsupportedOperationException();
  }
  public MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {
    throw new UnsupportedOperationException();
  }
  public boolean isCollection() {
    return true;
  }
  public void add(Object element) {
    object.add(element);
  }
  public <E> void addAll(List<E> element) {
    object.addAll(element);
  }
}

Pay attention to the get method, which always throws UnsupportedOperationException. So for the parameters of Collection type, all collection.property values ​​will receive an exception, don't step on the pit.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325301757&siteId=291194637