IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Mybatis动态SQL踩坑记 -> 正文阅读

[大数据]Mybatis动态SQL踩坑记

问题描述

使用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

/**
     * 判断表达式对应的值,是否为 true
     *
     * @param expression mapper.xml中的条件表达式
     * @param parameterObject 参数对象
     * @return 是否为 true
     */
    public boolean evaluateBoolean(String expression, Object parameterObject) {
        // 获得表达式对应的值
        Object value = OgnlCache.getValue(expression, parameterObject);
        // 如果是 Boolean 类型,直接判断
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        // 如果是 Number 类型,则判断不等于 0
        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”属性。
异常堆栈如下:
在这里插入图片描述

原因探索

到这里出现了两个问题:

  1. 为什么写成<if test="name != nul">",会把“nul”解析为对象的属性并会报错。
  2. 为什么在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);
            }
        }
    }
    
    /**
    *
    * @param cls 参数对象的类型
    */
	public static PropertyAccessor getPropertyAccessor(Class cls) throws OgnlException {
		// _propertyAccessors为已注册的属性访问器
        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

/**
    *
    * @param handlers 为已注册的属性访问器
    */
	private static Object getHandler(Class forClass, ClassCache handlers) {
        Object answer = null;
        // 根据参数类型或者其父类或者实现的接口来寻找对应的属性处理器PropertyAccessor
        // 1.如果从handlers找到就直接返回,找不到则继续向下执行
        if ((answer = handlers.get(forClass)) == null) {
            synchronized(handlers) {
                if ((answer = handlers.get(forClass)) == null) {
                    Class keyFound;
                    // 2.如果是数组
                    if (forClass.isArray()) {
                        answer = handlers.get(Object[].class);
                        keyFound = null;
                    } else {
                        keyFound = forClass;

                        label51:
                        // 3.向上查找有没有其父类对应的属性访问器
                        for(Class c = forClass; c != null; c = c.getSuperclass()) {
                            answer = handlers.get(c);
                            if (answer != null) {
                                keyFound = c;
                                break;
                            }

							// 4.查找有没有实现的接口对应的属性访问器
                            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) {
                    	// 直接根据forClass没找到,但是根据上述查找过程找到了,就把		
                    	// forClass和找到的访问器对应起来放到访问器注册表
                        handlers.put(forClass, answer);
                    }
                }
            }
        }

        return answer;
    }

上面代码出现了一个很重要的对象,即_propertyAccessors(属性访问器注册表)。
先来看看这里面默认注册了哪些访问器:
OgnlRuntime.java

		PropertyAccessor p = new ArrayPropertyAccessor();
		// Object类型对应ObjectPropertyAccessor这个访问器
        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$ContextMapContextAccessor的对应关系已经注册到注册表了:
DynamicContext.java

	static {
        // 设置 OGNL 的属性访问器
        OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
    }

到这里找到了两个关键的对象:ObjectPropertyAccessorContextAccessor

接下来就主要看看这两个访问器在获取属性值的时候有什么区别。
先看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) {
        	// 如果没获取到值就报错NoSuchPropertyException
            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;
            // 先尝试通过该属性的getter方法获取属性值
            if ((result = OgnlRuntime.getMethodValue(ognlContext, target, name, true)) == OgnlRuntime.NotFound) {
            // 如果通过getter不能获取,则通过反射获取
                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其实就是ContextMap
            Map map = (Map) target;

            // 重点!!!优先从 ContextMap 中,获得属性
            Object result = map.get(name);
            
            if (map.containsKey(name) || result != null) {
                return result;
            }
            ...
        }

DynamicContext$ContextMap.java

		public Object get(Object key) {
            // 如果有 key 对应的值,直接获得
            String strKey = (String) key;
            if (super.containsKey(strKey)) {
                return super.get(strKey);
            }

            // 从 parameterMetaObject 中,获得 key 对应的属性
            if (parameterMetaObject != null) {
                // 重点!!!
                return parameterMetaObject.getValue(strKey);
            }

            return null;
        }

进入parameterMetaObject.getValue(strKey)

	/**
	*
	* @param name 属性名
	*/
	public Object getValue(String name) {
        // 创建 PropertyTokenizer 对象,对 name 分词
        PropertyTokenizer prop = new PropertyTokenizer(name);
        // 有子表达式
        if (prop.hasNext()) {
            // 创建 MetaObject 对象
            MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
            // 递归判断子表达式 children ,获取值
            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 {
        	// 重点!!!获取属性的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);
        }
    }

依次进入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'

思考总结

经过上面的原因探索可以知道下面三件事:

  1. 对于条件表达式中的“null”,Ognl会将其解析成nul对象,其他的则会当成对象的属性来解析。
  2. mybatis对于属性值的获取有自己的属性访问器DynamicContext$ContextAccessor
  3. mybatis中条件表达式,如<if test="name != nickName"></>左右两边其实都可以是参数对象的属性。
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 19:08:56  更:2022-06-29 19:10:19 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/27 8:05:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码