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

lanluyug 2019-01-12

开端

今天小朋友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}表达式来获取值。其中包括递归查找对象属性的过程。

1

2

3

4

5

6

7

8

9

10

11

12

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. 使用<bind />

<bind name="idCount" value="ids.length" />

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<Object> answer = new ArrayList<Object>();

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取值,都会收到一个异常,千万不要踩坑哦。

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

相关推荐