MyBatis解析XML标签及占位符相关源码剖析

Stella981
• 阅读 564

开端

今天小朋友X在开发过程中遇到了一个bug,并给mybatis提了一个ISSUE:throw ReflectionException when using #{array.length}

大致说明下该问题,在mapper.xml中,使用#{array.length}来获取数组的长度时,会报出ReflectionException。 代码:

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

对应的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>

下面结合源码对该问题进行分析

分析

xml中有两处使用了length,那么这个报错究竟是哪个引起的呢?

尝试把test条件去掉,limit保留后,依然报错。那么可定位出报错是#{ids.length}导致的。

由此引出了两个问题:

  1. XML标签中条件是如何解析的(扩展,foreach是如何解析的数组和集合)
  2. #{ids.length}是如何解析的

带着这两个问题,我们进入源码

第一部分 XML标签的解析

在类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);
}

在每个对应的Handler中,有相应的处理逻辑。

以IfHandler为例:

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);
  }
}

在这里主要生成了IfSqlNode,解析在相应的类中

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使用的是OGNL表达式来运算的。

再举一个高级的例子:ForEachSqlNode,其中包括对数组和Collection以及Map的解析,核心是通过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.");
}

中间有个有意思的注释,参考后记1.

第二部分 ${},#{}的解析

首先需要明确:

  1. ${}: 使用OGNL动态执行内容,结果拼在SQL中
  2. #{}: 作为参数标记符解析,把解析内容作为prepareStatement的参数。

对于xml标签,其中的表达式也是使用的${}的解析方式,使用OGNL表达式来解析。

对于参数标记符解析,mybatis使用的是自己设计的解析器,使用反射机制获取各种属性。

以#{bean.property}为例,使用反射取到bean的属性property值。他的解析过程如下:

  1. BaseExecutor.createCacheKey方法

这个方法中遍历解析所有的参数映射关系,并根据#{propertyName}中的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);

这一步是为了获取MetaObject对象,该对象用于根据object类型来包装object对象,以便后续根据#{propertyName}表达式来获取值。其中包括递归查找对象属性的过程。

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);

这一步生成MetaObject对象,内部根据object的具体类型,分别生成不同的objectWrapper对象。

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);

这一步真正获取了#{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);

通过第三步中生成的objectWrapper来获取真正的属性值,不同wrapper获取方式不同,以beanWrapper为例:

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);
  }
}

至此,#{propertyName}的解析就完成了。${}则是直接使用的OGNL表达式解析,不详细解析了。

结论

下面回到问题,仔细分析后,得到错误原因:

上面第三步中,生成的ObjectWrapper类型是BeanWrapper,而BeanWrapper中获取属性值length,会调用反射尝试获取getter方法,并执行。对于一个数组类型的对象,当然是不可能有getter方法的(仅指java)。

而在test中的ids.length则没有问题,是因为test中的表达式是使用的OGNL来执行的。参考第一部分的ExpressionEvaluator。最后的则是执行的第二部分中的代码逻辑,故报错。

解决

解决方法有三种:

  1. 更换#{array.length}为${array.length}即可解决。

  2. 使用

    LIMIT #{idCount}

读者可以尝试去看下bind标签的处理逻辑。 3. 如上面一样,增加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异常。
}

这里通过判断属性值为"length"来获取数组长度,其他均抛出异常。这样便支持了#{}占位符中数组长度的获取。

后记

  1. 有意思的注释

    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 answer = new ArrayList(); for (int i = 0; i < size; i++) { Object o = Array.get(value, i); answer.add(o); } return answer; }

    注释是什么意思呢?意思是使用Arrays.asList()来转换数组为List时,可能会抛出ClassCastException。当数组为原始类型数组时,必然会抛出ClassCastException异常。

    详细分析下原因,看Arrays.asList()方法

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

    根据泛型消除原则,这里实际接收的参数类型为Obejct[],而数组类型是有特殊的继承关系的。

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

    当A数组的元素类型1是类型2的子类时,A数组是类型2数组类型的实例。即当类型1是类型2的之类时,类型1数组类型是类型2数组类型的子类。

    但是有个特殊情况,一些原生类型(int,char...)的数组,并不是任何类型数组的子类,在把int[]强转为Object[]时,必然会抛出ClassCastException异常。虽然原始类型在用Object接收时会进行自动装箱的处理,但是原始类型的数组并不会进行自动装箱,这里就是根本原因了。这也就是这个注释出现的原因,以及要去遍历数组用Object取元素并放入List的根本原因。

    1. 一堆if else if分支

    原因基本同上,每个原始类型的数组类型都是一个特别的类型,故都需要进行特殊对待。

    1. CollectionWrapper的注意事项

    直接看代码:

    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);
      }
    }
    

    注意get方法,固定抛出UnsupportedOperationException异常。所以对于Collection类型的参数,所有的collection.property取值,都会收到一个异常,千万不要踩坑哦。

    点赞
    收藏
    评论区
    推荐文章
    待兔 待兔
    4个月前
    手写Java HashMap源码
    HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
    Easter79 Easter79
    3年前
    springboot集成mybatisplus
    介绍:     MybatisPlus(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Fbaomidou%2Fmybatisplus)(简称MP)是一个 Mybatis(https://www.oschina.net/action/G
    Stella981 Stella981
    3年前
    Spring Boot 与 Kotlin 整合MyBatis
    最近使用jpa比较多,再看看mybatis的xml方式写sql觉得不爽,接口定义与映射离散在不同文件中,使得阅读起来并不是特别方便。因此使用SpringBoot去整合MyBatis,在注解里写sql参考《我的第一个Kotlin应用》(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%
    Wesley13 Wesley13
    3年前
    1:dubbo集成spring
    dubbo源码地址(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Falibaba%2Fdubbo)查找解析类DubboBeanDefinitionParserdubbo通过spring提供的自定义namespace来解析自己定义的标签,读取META
    Stella981 Stella981
    3年前
    Mybatis源码分析(一)
    准备在阅读源码前,需要先clone源码地址:https://github.com/mybatis/mybatis3(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Fmybatis%2Fmybatis3)Mybatis框架使用大量常见的设
    Stella981 Stella981
    3年前
    Mybatis 通用Crud
    前言(说明)源码地址:https://github.com/LittleNewbie/portal(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2FLittleNewbie%2Fportal)mybatis版本 3.2.6mybatis
    Stella981 Stella981
    3年前
    Mybatis 面试题
    题目:1.什么是Mybatis? Mybatis27题(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fwilliamjie%2Fp%2F11190005.html)2.Mybaits的优点 Mybatis27题(https://w
    Stella981 Stella981
    3年前
    Mybatis源码解析,一步一步从浅入深(五):mapper节点的解析
    在上一篇文章Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fzhangchengzi%2Fp%2F9674527.html)
    Stella981 Stella981
    3年前
    Mybatis源码解析,一步一步从浅入深(七):执行查询
    一,前言  我们在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fzhangchengzi%2Fp%2F9672922.html)的最后一步说到执行查询的关键代码:
    Stella981 Stella981
    3年前
    Extm MyBatis增强工具
    Extm是一个MyBatis(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Flinks.jianshu.com%2Fgo%3Fto%3Dhttp%253A%252F%252Fwww.mybatis.org%252Fmybatis3%252F)的增强工具,在MyBatis的