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

阅读数:59 2019 年 9 月 22 日 22:54

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

今天小朋友 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}导致的。

由此引出了两个问题:

  • XML 标签中条件是如何解析的 (扩展,foreach 是如何解析的数组和集合)
  • #{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.");
}

【注】:中间有个有意思的注释,参考后记注释。

${},#{}的解析

首先需要明确:

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

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

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

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

  • 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;
}
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);
}
}
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);
}
}
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);
}
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。最后的则是执行的第二部分中的代码逻辑,故报错。

解决

解决方法有三种:

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

使用

复制代码
<bind name="idCount" value="ids.length" />
LIMIT #{idCount}

【注】:读者可以尝试去看下 bind 标签的处理逻辑。

如上面一样,增加 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" 来获取数组长度,其他均抛出异常。这样便支持了 static sql 中数组长度的获取。

后记

有意思的注释

复制代码
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 的根本原因。

一堆 if else if 分支

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

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

作者介绍:
王耀,人力基础产品技术中心,16 年 2 月加入链家,任职 JAVA 研发工程师,开源框架 FastBootWeixin 核心开发者。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/dCiuWf6_eCpBaTj4MHPf8Q

评论

发布