MyBatis 解析 XML 标签及占位符相关源码剖析,mybatisxml
MyBatis 解析 XML 标签及占位符相关源码剖析,mybatisxml
开端
今天小朋友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}表达式来获取值。其中包括递归查找对象属性的过程。
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取值,都会收到一个异常,千万不要踩坑哦。
用户点评