问题描述
使用mybatis进行数据库查询的时候,mapper.xml部分配置如下:
<if test="name != nul">
name = #{name}
</if>
执行以后发现控制台报错:
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named ‘nul’ in ‘class com.jarry.entity.Book’
意思是Book类里面找不到属性“nul”的get方法😣,然而我的Book类里面压根就没定义“nul”属性。 其实这里一眼就看出来是因为错把“null”写成了“nul”才会报错。
但是本着知其然也要知其所以然的心态,决定探究一下为什么会报这个错。
问题复现
根据异常堆栈找到相关的类和方法: ExpressionEvaluator.java
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
这个方法就是判断上面<if test="name != nul">" 是否成立的。
mybatis内部使用的表达式引擎是Ognl,所以会通过Ognl来解析并执行该条件表达式。
因此可以模拟直接使用Ognl解析"name != nul" 这个表达式来复现这个问题。 代码如下:
Book book = new Book(1,null,"xujian");
System.out.println(OgnlCache.getValue("name == nul",book));
然而执行之后报的错误却是这样的:
Cause: ognl.NoSuchPropertyException: com.jarry.entity.Book.nul
意思是Book没有这个“nul”属性。 异常堆栈如下:
原因探索
到这里出现了两个问题:
- 为什么写成
<if test="name != nul">" ,会把“nul”解析为对象的属性并会报错。 - 为什么在mybatis下解析
"name != nul" 和Ognl直接解析报的错误不一样。
问题NO.1的探索
这个问题主要在于Ognl对于"name != nul" 的解析。 OgnlCache.java
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
debug查看node = Ognl.parseExpression(expression); 解析的结果: 可以看到这里居然把“nul”解析成了一个属性。
对比看下"name != null" 这个正确表达式的解析结果: 这里会把“null”解析成一个常量,进而会把他处理成null 对象。
正是因为把“nul”解析成了属性,所以才会去上下文对象中寻找这个属性,进而报错。
问题NO.2的探索
既然把“nul”解析成了属性(在Ognl中体现为ASTProperty这个Node节点),那就从获取这个属性值的地方入手。
根据上面的异常堆栈可以找到获取属性值的地方: ASTProperty.java
protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {
Object property = this.getProperty(context, source);
Object result = OgnlRuntime.getProperty(context, source, property);
if (result == null) {
result = OgnlRuntime.getNullHandler(OgnlRuntime.getTargetClass(source)).nullPropertyValue(context, source, property);
}
return result;
}
OgnlRuntime.java
public static Object getProperty(OgnlContext context, Object source, Object name) throws OgnlException {
if (source == null) {
throw new OgnlException("source is null for getProperty(null, \"" + name + "\")");
} else {
PropertyAccessor accessor;
if ((accessor = getPropertyAccessor(getTargetClass(source))) == null) {
throw new OgnlException("No property accessor for " + getTargetClass(source).getName());
} else {
return accessor.getProperty(context, source, name);
}
}
}
public static PropertyAccessor getPropertyAccessor(Class cls) throws OgnlException {
PropertyAccessor answer = (PropertyAccessor)getHandler(cls, _propertyAccessors);
if (answer != null) {
return answer;
} else {
throw new OgnlException("No property accessor for class " + cls);
}
}
PropertyAccessor answer = (PropertyAccessor)getHandler(cls, _propertyAccessors); 这里会通过参数对象的类型从已注册的属性访问器表中获取对应的属性访问器。 OgnlRuntime.java
private static Object getHandler(Class forClass, ClassCache handlers) {
Object answer = null;
if ((answer = handlers.get(forClass)) == null) {
synchronized(handlers) {
if ((answer = handlers.get(forClass)) == null) {
Class keyFound;
if (forClass.isArray()) {
answer = handlers.get(Object[].class);
keyFound = null;
} else {
keyFound = forClass;
label51:
for(Class c = forClass; c != null; c = c.getSuperclass()) {
answer = handlers.get(c);
if (answer != null) {
keyFound = c;
break;
}
Class[] interfaces = c.getInterfaces();
int index = 0;
for(int count = interfaces.length; index < count; ++index) {
Class iface = interfaces[index];
answer = handlers.get(iface);
if (answer == null) {
answer = getHandler(iface, handlers);
}
if (answer != null) {
keyFound = iface;
break label51;
}
}
}
}
if (answer != null && keyFound != forClass) {
handlers.put(forClass, answer);
}
}
}
}
return answer;
}
上面代码出现了一个很重要的对象,即_propertyAccessors (属性访问器注册表)。 先来看看这里面默认注册了哪些访问器: OgnlRuntime.java
PropertyAccessor p = new ArrayPropertyAccessor();
setPropertyAccessor(Object.class, new ObjectPropertyAccessor());
setPropertyAccessor(byte[].class, p);
setPropertyAccessor(short[].class, p);
setPropertyAccessor(char[].class, p);
setPropertyAccessor(int[].class, p);
setPropertyAccessor(long[].class, p);
setPropertyAccessor(float[].class, p);
setPropertyAccessor(double[].class, p);
setPropertyAccessor(Object[].class, p);
setPropertyAccessor(List.class, new ListPropertyAccessor());
setPropertyAccessor(Map.class, new MapPropertyAccessor());
setPropertyAccessor(Set.class, new SetPropertyAccessor());
setPropertyAccessor(Iterator.class, new IteratorPropertyAccessor());
setPropertyAccessor(Enumeration.class, new EnumerationPropertyAccessor());
在复现的时候我们的参数对象是Book,因为Book的父类是Object.class,所以根据方法getHandler(Class forClass, ClassCache handlers) 找到的访问器就是ObjectPropertyAccessor 。
而通过debug可以看到在实际场景中mybatis会把原始的参数对象包装进DynamicContext$ContextMap 对象中:
{
"_parameter":Book->{"id":1,"name":null,"author":"xujian"},
"_databaseId":null
}
所以这里要找的是DynamicContext$ContextMap 对应的属性访问器,按照上面的查找逻辑,按道理也应该找到的是ObjectPropertyAccessor 。 但事实上找到的却是ContextAccessor ,其实在之前已经将DynamicContext$ContextMap 和ContextAccessor 的对应关系已经注册到注册表了: DynamicContext.java
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
到这里找到了两个关键的对象:ObjectPropertyAccessor 和ContextAccessor 。
接下来就主要看看这两个访问器在获取属性值的时候有什么区别。 先看ObjectPropertyAccessor.java
public Object getProperty(Map context, Object target, Object oname) throws OgnlException {
Object result = null;
String name = oname.toString();
result = this.getPossibleProperty(context, target, name);
if (result == OgnlRuntime.NotFound) {
throw new NoSuchPropertyException(target, name);
} else {
return result;
}
}
public Object getPossibleProperty(Map context, Object target, String name) throws OgnlException {
OgnlContext ognlContext = (OgnlContext)context;
try {
Object result;
if ((result = OgnlRuntime.getMethodValue(ognlContext, target, name, true)) == OgnlRuntime.NotFound) {
result = OgnlRuntime.getFieldValue(ognlContext, target, name, true);
}
return result;
} catch (IntrospectionException var7) {
throw new OgnlException(name, var7);
} catch (OgnlException var8) {
throw var8;
} catch (Exception var9) {
throw new OgnlException(name, var9);
}
}
从上面代码可以看出来,当属性名称是nul 时,肯定找不到这个属性,ObjectPropertyAccessor 就会报错NoSuchPropertyException ,这就和最开始复现的时候报的错误对上了。
再来看DynamicContext$ContextAccessor.java
public Object getProperty(Map context, Object target, Object name)
throws OgnlException {
Map map = (Map) target;
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
...
}
DynamicContext$ContextMap.java
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
return parameterMetaObject.getValue(strKey);
}
return null;
}
进入parameterMetaObject.getValue(strKey)
public Object getValue(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return null;
} else {
return metaValue.getValue(prop.getChildren());
}
} else {
return objectWrapper.get(prop);
}
}
再进入BeanWrapper.java
public Object get(PropertyTokenizer prop) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
return getCollectionValue(prop, collection);
} else {
return getBeanProperty(prop, object);
}
}
private Object getBeanProperty(PropertyTokenizer prop, Object object) {
try {
Invoker method = metaClass.getGetInvoker(prop.getName());
try {
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);
}
}
依次进入MetaClass#getGetInvoker(prop.getName()) ->Reflector#getGetInvoker(String propertyName)
Invoker method = getMethods.get(propertyName);
if (method == null) {
throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
}
return method;
其中getMethods 是一个Map<String/*属性名*/, Invoker/*属性值获取方法*/> ,参数对象的getter方法会保存在这里。 当属性名称是nul 的时候,这里当然就找不到对应的getter方法,就会报错There is no getter for property named 'nul' in 'class com.jarry.entity.Book' 。
思考总结
经过上面的原因探索可以知道下面三件事:
- 对于条件表达式中的“null”,Ognl会将其解析成
nul 对象,其他的则会当成对象的属性来解析。 - mybatis对于属性值的获取有自己的属性访问器
DynamicContext$ContextAccessor 。 - mybatis中条件表达式,如
<if test="name != nickName"></> 左右两边其实都可以是参数对象的属性。
|