• Mybatis动态SQL踩坑记


    问题描述

    使用mybatis进行数据库查询的时候,mapper.xml部分配置如下:

    <if test="name != nul">
    	name = #{name}
    </if>
    
    • 1
    • 2
    • 3

    执行以后发现控制台报错:

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这个方法就是判断上面<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));
    
    • 1
    • 2

    然而执行之后报的错误却是这样的:

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    上面代码出现了一个很重要的对象,即_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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在复现的时候我们的参数对象是Book,因为Book的父类是Object.class,所以根据方法getHandler(Class forClass, ClassCache handlers)找到的访问器就是ObjectPropertyAccessor

    而通过debug可以看到在实际场景中mybatis会把原始的参数对象包装进DynamicContext$ContextMap对象中:

    {
    	"_parameter":Book->{"id":1,"name":null,"author":"xujian"},
    	"_databaseId":null
    }
    
    • 1
    • 2
    • 3
    • 4

    所以这里要找的是DynamicContext$ContextMap对应的属性访问器,按照上面的查找逻辑,按道理也应该找到的是ObjectPropertyAccessor
    但事实上找到的却是ContextAccessor,其实在之前已经将DynamicContext$ContextMapContextAccessor的对应关系已经注册到注册表了:
    DynamicContext.java

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

    到这里找到了两个关键的对象: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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    从上面代码可以看出来,当属性名称是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;
                }
                ...
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    进入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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    再进入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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    依次进入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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中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"></>左右两边其实都可以是参数对象的属性。
  • 相关阅读:
    五个元素的整形数组
    最简单的git图解(多远程仓库)
    Spring Event 观察者模式, 业务解耦神器
    Python标准库之pickle
    SwiftUI的context Menu
    52_Pandas处理日期和时间列(字符串转换、日期提取等)
    计算机毕业设计Java交通非现场执法系统(源码+系统+mysql数据库+lw文档)
    【算法笔记】树状数组/Binary Indexed Tree/Fenwick Tree
    使用c#将aj-report桌面化1
    Ajax及跨域请求
  • 原文地址:https://blog.csdn.net/qq_18515155/article/details/125421410