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.");
}
中间有个有意思的注释,参考后记1.
第二部分 ${},#{}的解析
首先需要明确:
- ${}: 使用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}表达式来获取值。其中包括递归查找对象属性的过程。
1
2
3
4
5
6
7
8
9
10
11
12
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 />
<bind name="idCount" value="ids.length" />
LIMIT #{idCount}
读者可以尝试去看下bind标签的处理逻辑。 3. 如上面一样,增加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”来获取数组长度,其他均抛出异常。这样便支持了#{}占位符中数组长度的获取。
后记
- 有意思的注释
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取值,都会收到一个异常,千万不要踩坑哦。