AICon 上海站|90%日程已就绪,解锁Al未来! 了解详情
写点什么

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

评论

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

阿里云中间件开源往事

阿里巴巴中间件

阿里云 开源 中间件

“去虚向实”大潮下,百度智能云向实而生

科技新知

抖音或将推出独立种草社区平台:会不会成为第二个小红书

石头IT视角

【愚公系列】2022年7月 Go教学课程 005-变量

愚公搬代码

7月月更

1500万员工轻松管理,云原生数据库GaussDB让HR办公更高效

华为云开发者联盟

数据库 后端

架构实战营模块 6 作业

Roy

架构实战营

ServiceMesh主要解决的三大痛点

阿泽🧸

Service Mesh 7月月更

一个酷酷的“幽灵”控制台工具

为自己带盐

C# 控制台 7月月更

SchedulX V1.4.0及SaaS版发布,免费体验降本增效高级功能!

星汉未来

DevOps 运维 k8s IT FinOps

阿里云易立:云原生如何破解企业降本提效难题?

阿里巴巴中间件

阿里云 架构 云原生

Flutter3.0了,小程序不止于移动应用跨端运行

Speedoooo

flutter 小程序 移动开发 小程序容器 跨端运行

小程序能运行在自有App中,且实现直播和连麦?

Speedoooo

小程序 直播 移动开发 小程序容器 连麦

【刷题记录】2. 两数相加

WangNing

7月月更

【写给初发论文的人】撰写综述性科技论文常见问题

左手の明天

论文阅读 论文 论文写作 研究论文 论文撰写

企业数字化转型,低代码是“趋势”还是“陷阱”?

云智慧AIOps社区

大前端 低代码 云开发

张平安:加快云上数字创新,共建产业智慧生态

华为云开发者联盟

云计算 后端 SaaS 华为云

枚举通用接口&枚举使用规范

靠谱的程序员

枚举 企业应用 企业级应用

组织实战攻防演练的5个阶段

穿过生命散发芬芳

攻防演练 7月月更

牛客java选择题每日打卡Day8

京与旧铺

7月月更

java零基础入门-Scanner类

喵手

Java’ 7月月更

从0开始创建小程序

小恺

7月月更

Linux 下的传统 IPC 通信原理

北洋

Andriod 7月月更

用头像模仿天狗食月

急需上岸的小谢

7月月更

leetcode 53. Maximum Subarray 最大子数组和(中等)

okokabcd

LeetCode 动态规划 数据结构与算法

当 Knative 遇见 WebAssembly

阿里巴巴中间件

阿里云 容器 云原生 Knative WebAssenbly

从解析HTML开始,破解页面渲染时间长难题

华为云开发者联盟

html 前端 web开发 网页

Web开发小妙招:巧用ThreadLocal规避层层传值

华为云开发者联盟

Java 前端 web开发

HAVE FUN | “飞船计划”活动最新进展

SOFAStack

微服务架构 开源软件 新手引导

华为小米互“抄作业”

科技新知

offer如何选择该考虑哪些因素

KEY.L

7月月更

谈谈讲清楚这件事的重要性

阿里巴巴中间件

阿里云 技术 云原生

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