NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

  • 2019-09-22
  • 本文字数:8497 字

    阅读完需:约 28 分钟

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


2019-09-22 22:541081

评论

发布
暂无评论
发现更多内容

软件架构第一章总结

itrickzhang

【架构师训练营-作业-1】食堂就餐卡系统设计

小动物

系统设计 极客大学架构师训练营 作业

体验碎片-06/06

黄大路

产品经理

架构方法:运用合适的工具表达设计

SkyeDance

极客大学架构师训练营

repo 导出本地 git tag 给他人

zqb-all

git

如何用一台 MacBook 创造高额年化收益 | ETH2.0 Staking 教程

陈东泽 EuryChen

区块链 Ethereum

【ARTS打卡】Week02

Rex

述职报告的一些建议

黄大路

职场

食堂就餐卡系统设计

上山砍柴

极客大学架构师训练营

在 Windows WSL 2 中使用 Docker Desktop

FeiLong

Docker WSL2

程序员的晚餐 | 6 月 4 日 最好吃的土豆

清远

初识 Docker 与安装 | Docker 系列

AlwaysBeta

Docker

近邻算法介绍

黄大路

数据挖掘 数据分析

食堂就餐卡系统设计-uml练习

森林

[ARTS打卡] week 02

Mau

ARTS 打卡计划

Java 25周年:波澜壮阔的25年

北风

「Java 25周年」

架构设计文档之食堂就餐卡系统设计

itrickzhang

架构设计 架构文档 架构样例 架构分析 架构总结

2020年6月7日 接口、lambda表达式与内部类

瑞克与莫迪

《OKR工作法》读书笔记

大饼土博

读书笔记 管理 OKR

架构师课程第一周作业

杉松壁

架构师训练营第一周学习总结

王鑫龙

极客大学架构师训练营

利其器

宋胖子

IDEA

MySQL 笔记(一)基础架构

奈何花开

Java MySQL

优秀架构师具备的能力

李小匪

极客大学架构师训练营

架构文档

陈皮

架构 极客大学架构师训练营

UML练习1 食堂就餐卡系统设计「架构师训练营」

Young

「架构师训练营」第1周作业 - 食堂就餐卡系统设计

guoguo 👻

极客大学架构师训练营

教你动手写UDP协议栈

Rice嵌入式开发技术分享

TCP udp 协议栈

程序员摆地摊?你别痴心妄想了,还不如当「在地青年」呢

非著名程序员

程序员 提升认知 职业规划 认知提升

ARTS 第 1 周

乌拉里

ARTS 打卡计划

程序员的晚餐 | 6 月 5 日 爆炒鱿鱼

清远

美食

MyBatis解析XML标签及占位符相关源码剖析_文化 & 方法_王耀_InfoQ精选文章