今天小朋友 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
更多内容推荐
04|增强 IoC 容器:如何让我们的 Spring 支持注解?
实现Autowired注解,并用这个方式进行依赖注入
2023-03-20
15|mBatis:如何将 SQL 语句配置化?
mBatis:如何将SQL语句配置化?
2023-04-14
那些年,我们踩过的 Java 坑
同一个代码“坑”,踩第一次叫长了经验,踩第二次叫加深印象,踩第三次叫不长心眼,踩三次以上就叫不可救药。
Golang 如何进行类型检查
我们在上一节中介绍了 Golang 的第一个编译阶段
这些鲜为人知的 JavaScript 特性,你知道多少?
我是一个JavaScript老用户,直到现在,我仍然会时不时地发现一些我从来都不知道的隐藏语法或技巧。
14|增强模板:如何抽取专门的部件完成专门的任务?
如何抽取专门的部件完成专门的任务?
2023-04-12
MyBatis 版本升级引发的线上告警回顾及原理分析
本文介绍处理一次MyBatis版本升级引发的线上告警经历。
大部分教程不会告诉你的 12 个 JS 技巧
在这篇文章中,作者将分享12个非常有用的JavaScript技巧,可以帮助你写出简洁且高性能的代码。
10|数据绑定: 如何自动转换传入的参数?
自动转换传入的参数
2023-04-03
Android 使用 Retrofit+Gson 的数据解析研究
1背景服务端下发字段和客户端字段类型定义不一致,造成整个数据解析失败;异构列表类型数据的解析方式。这里异构列表是指:列表中包含的每一项数据的java实体类可以不同。
Mybatis【2.1】-- 从读取流到创建 SqlSession 发生了什么?
我们使用sqlSession之前,需要去获取配置文件,获取InputStream输入流,通过SqlSessionFactoryBuilder获取sqlSessionFactory对象,从而获取sqlSession。
2020-11-16
19|Pointcut :如何批量匹配代理方法?
Pointcut :如何批量匹配代理方法?
2023-04-24
11|ModelAndView :如何将处理结果返回给前端?
将处理结果返回给前端
2023-04-05
这 10 行比较字符串相等的代码给我整懵逼了,不信你也来看看
用种题引点来,过不看,看否你所获有获请论留言没获下末直吃*哈,你信
2020-06-14
MyBatis 解析 XML 标签及占位符相关源码剖析
大致说明下该问题,在mapper.xml中,使用#{array.length}来获取数组的长度时,会报出ReflectionException。
游戏夜读 | 有哪几种游戏玩家?
“请问您觉得自己是哪种游戏玩家呢?”
2020-05-07
Dubbo 源码解析之 SPI(一):扩展类的加载过程(下)
Dubbo的SPI简单么?
一篇文章深入理解 JDK8 HashMap
笔者在上一篇文章《深入理解JDK7 HashMap》中详细解析了HashMap在JDK7中的实现原理,主要是围绕其put、get、resize、transfer等方法,本文将继续解析HashMap在JDK8中的具体实现,首先也将从put、get、resize等方法出发,着重解析HashMap在JDK7和JDK8中的具体
2020-07-09
某某大龄程序员被字节面试官怒喷“废物”,他得知真相之后都懵了
这是我从去字节面试最后凉凉的朋友们那,挑出来一个特别典型的面试字节凉两次的衰仔;从他那获取的经验从而写了这篇文章以供大家参考!
2021-02-20
SpringSecurity 安全控件使用指南,建议细读
获取UserDetails类,该类中包含用户认证相关等信息。
2021-11-10
推荐阅读
20|AutoProxyCreator:如何自动添加动态代理?
2023-04-26
21|再回首: 如何实现 Spring AOP?
2023-04-28
MyBatis3 源码解析 (7)TypeHandler 注册与获取
2022-02-16
一个关于 += 的谜题
2022-02-28
如何告别丑陋判空?一个 Optional 类就能搞定!
2023-01-07
并发编程 -FutureTask 解析 | 京东物流技术团队
2023-07-27
01- 线程安全 -synchronized 原理剖析
2023-09-26
电子书
大厂实战PPT下载
换一换 王松 | 未来智能 CTO
Michael “Monty” Widenius | MySQL 数据库 原始版本主要作者,MariaDB Foundation CTO
孙东 | 58同城 SaaS 应用大前端负责人
评论