阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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:541075

评论

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

一年时间过去了,LiveData真的被Flow代替了吗? LiveData会被废弃吗?

编程的平行世界

android android jetpack

git clone出错--必看贴

坚果

git Mac git 学习 7月月更

STM32+ENC28J60+UIP协议栈实现WEB服务器示例

DS小龙哥

7月月更

架构师成长:关于我在 ArchSummit 大会收获了什么

宇宙之一粟

架构 个人感悟 ArchSummit 7月月更

关于Web响应式设计

程序员海军

Web 7月月更 响应式设计

Okaleido tiger NFT即将登录Binance NFT平台,NFT权益时代即将开启

鳄鱼视界

iOS中内存管理(Autoreleasepool)

NewBoy

ios 前端 移动端 iOS 知识体系 7月月更

Java RMI

沃德

Java 程序员 7月月更

【MySql 实战】以 sql 的方式多表联动更新数据

安逸的咸鱼

MySQL 实战 7月月更

Okaleido tiger NFT即将登录Binance NFT平台,后市持续看好

EOSdreamer111

java零基础入门-java8新特性(中篇)

喵手

Java 7月月更

前端网络之跨域请求

Jason199

跨域 7月月更

树莓派3B搭建Flink集群

程序员欣宸

Java flink 树莓派 7月月更

TableWidget 排序的多种方式

小肉球

qt 7月月更

如何分析并设计性能测试场景

老张

性能测试 需求分析

Docker小白的福音:50条Docker命令清单,干就完了!

wljslmz

Docker Linux Docker 镜像 7月月更

Starfish OS:以现实为纽带,打造元宇宙新范式

EOSdreamer111

Qt | Qt的项目文件.pro文件详解

YOLO.

qt 7月月更

【函数式编程实战】(一)Java演变与函数式编程

小明Java问道之路

Lambda stream 函数式编程 7月月更

Web3流量聚合平台Starfish OS,给玩家元宇宙新范式体验

股市老人

正则什么的,你让我写,我会难受,你让我用,真香!

掘金安东尼

前端 正则 7月月更

Java中关于多线程的知识点

Java学术趴

7月日更

王者荣耀商城异地多活架构设计

Geek_7a789a

C#入门系列(二十六) -- 程序集和命名空间

陈言必行

7月月更

云原生(九) | Devops篇之Jenkins安装与实战

Lansonli

云原生 7月月更

mysql进阶(十四) 批量更新与批量更新多条记录的不同值实现方法

No Silver Bullet

MySQL 数据库 7月月更 批量更新

LeetCode-94. 二叉树的中序遍历(java)

bug菌

Leet Code 7月月更

王者荣耀商城异地多活部署设计

Geek_e8bfe4

从日常小事看如何工作

耳东@Erdong

7月月更

Okaleido tiger NFT即将登录Binance NFT平台,你期待吗?

股市老人

架构实战营模块7作业

挖了蘑菇哩斯

架构实战营

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