报名参加CloudWeGo黑客松,奖金直推双丰收! 了解详情
写点什么

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

  • 2019-11-01
  • 本文字数:8492 字

    阅读完需:约 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-11-01 11:501054

评论

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

带你掌握4种Python排序算法

华为云开发者联盟

Python 编程 算法 排序 冒泡排序

浪潮云说丨叮!这是一份浪潮云物联网平台的简历,请查收!

云计算

深入浅出 LVS 负载均衡(四)实操 DR 模型、Keepalived DR 模型的高可用

UCloud技术

负载均衡

HarmonyOS 实战—服务卡片初体验

爱吃土豆丝的打工人

HarmonyOS 服务卡片 鸿蒙卡片

去中心化的互联网,区块链域名如何对抗在线审查

CECBC

Pandas高级教程之:处理缺失数据

程序那些事

Python 数据分析 pandas 程序那些事

anyRTC 重磅推出在线实时 K 歌解决方案

anyRTC开发者

音视频 WebRTC 实时通讯 在线KTV

超清音质实时会议系统的背后 ,深入剖析 AliCloudDenoise 语音增强算法

阿里云CloudImagine

阿里云 音视频 语音 视频会议 算法实践

如何设计好一个接口

🎄新

架构 设计 接口

作为新时代的Java工程师,你需要具备什么能力?

卢卡多多

Java 能力提升 6月日更 六月

同样都是使用接口,JAVA和Go差距咋就这么大呢?

面向加薪学习

原来 spring.xml 配置的 destroy-method 需要用到向虚拟机注册钩子来实现!

小傅哥

Java spring 注册虚拟机钩子 init-method destroy-method

高性能计算对生命科学研究有何帮助?

北鲲云

云计算 高性能计算 生命科学 虚拟筛选

☕【JVM监控实战】教会你使用Arthas(监控ElasticSearch服务)

码界西柚

JVM 故障定位 Arthas 6月日更

bzz|chia矿池挖矿系统APP开发搭建

薇電13242772558

区块链

JAVA面向对象(十一)--多态

加百利

Java 6月日更 多态

RS485通信如何设计EMC电路?

不脱发的程序猿

电路设计 通信总线 RS485 EMC设计 通信抗干扰

面试官:谈谈你对geohash的理解和如何实现附近人功能呢?

李阿柯

redis 面试 geohash

想要做好微服务化,这个核心对象要管好

BoCloud博云

微服务

构建WEB项目的 25 个HTML建议

devpoint

html 6月日更

Kafka 源码解析:Server 端的运行过程

华为云开发者联盟

kafka 网络 Server 端 SocketServer

破局团伙作案风险——图卷积神经网络(GCN)算法

索信达控股

金融科技 数字化转型 数据建模 风险管理 图卷积神经网络

使用 Java 编写 Apache APISIX 插件

API7.ai 技术团队

Java 云原生 后端 插件 网关

一进商场就迷路?ThingJS用室内导航拯救路痴!

ThingJS数字孪生引擎

程序员 大前端 可视化 3D可视化 数字孪生

云小课 | 云硬盘不用了如何处理?

华为云开发者联盟

华为云 云硬盘 退订 删除 回收站

融云年中大促 新老用户同享超值优惠

融云 RongCloud

拍乐云 x 美上美学|监管当下,如何回归教育本质,打造品质和体验?

拍乐云Pano

RTC

从网络平台到城市平台——城市数字化的另类思考

CECBC

Docker被谁干掉了?

BUG侦探

Docker 云计算 Go 语言

不知道我写的链表是否能看懂

八点半的Bruce.D

php 数据结构 链表

测试开发之网络篇-IP地址

禅道项目管理

IP 协议 IP地址

MyBatis解析XML标签及占位符相关源码剖析_数据库_王耀_InfoQ精选文章