难受,被 MyBatis 的 size() 坑惨了!

简介: 难受,被 MyBatis 的 size() 坑惨了!

Mybatis 是一个开源的轻量级半自动化 ORM 框架,使得面向对象应用程序与关系数据库的映射变得更加容易。MyBatis 使用 xml 描述符或注解将对象与存储过程或SQL 语句相结合。Mybatis 最大优点是应用程序与 Sql 进行解耦,sql 语句是写在 Xml Mapper 文件中。


OGNL 表达式在 Mybatis 当中应用非常广泛,其表达式的灵活性使得动态 Sql 功能的非常强大。OGNL 是 Object-Graph Navigation Language 的缩写,代表对象图导航语言。OGNL 是一种 EL 表达式语言,用于设置和获取 Java 对象的属性,并且可以对列表进行投影选择以及执行lambda表达式。


Ognl 类提供了许多简便方法用于执行表达式的。Struts2 发布的每个版本都会出现的新的高危可执行漏洞也是因为它使用了灵活的 OGNL 表达式。公司后端采用 Mybatis 作为数据访问层,所使用版本为 3.2.3。


线上环境业务系统在运行过程中出现了一个令人困惑的异常, 该异常时而出现时而不出现,构造各种 OGNL 表达式为空等特殊情况均不会重现该异常。具体异常堆栈信息如下:


### Error querying database.  Cause: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
### Cause: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:23) org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:107)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:98)
    at cn.com.shaobingmm.MybatisBugTest$2.run(MybatisBugTest.java:88)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java
    at:47)
    at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:29)
    at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:30)
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
    at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:51)
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:37)
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:275)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:79)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:104)
    ... 3 more
Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
    at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837)
    at org.apache.ibatis.ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:61)
    at org.apache.ibatis.ognl.OgnlRuntime.callMethod(OgnlRuntime.java:860)
    at org.apache.ibatis.ognl.ASTMethod.getValueBody(ASTMethod.java:73)
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
    at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:109)
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
    at org.apache.ibatis.ognl.ASTGreater.getValueBody(ASTGreater.java:49)
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
    at org.apache.ibatis.ognl.ASTAnd.getValueBody(ASTAnd.java:56)
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333)
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:413)
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:395)
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:45)
    ... 12 more


List 的 size() 方法明显是 public 为何还会出现不可访问的异常。该问题并不是每一次都会出现,经过多次尝试,该异常一直未在测试环境重现。该接口在完整调用链路中的出错次数占总调用次数的比率为 0.01%,无意中联想到并发问题在周期性时间内往往是概率性发生。编写模拟多线程环境并发读取公司列表测试代码:


<mapper namespace="CompanyMapper">
    <select id="getCompanysByIds"resultType="cn.com.shaobingmm.Company">
        select *
        from company
        <where>
            <if test="list != null and list.size() > 0">
                and id in
       <foreach collection="list" item="id" open="(" separator="," close=")">#{id}
</foreach>
            </if>
        </where>
    </select>
</mapper>


多线程并发环境下的压测代码


String resource = "mybatis-config.xml";
        InputStream in = null;
        try {
            in = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
            final List<Long> ids = Collections.singletonList(1L);
            final SqlSession session = sqlSessionFactory.openSession();
            final CountDownLatch mCountDownLatch = new CountDownLatch(1);
            for (int i = 0; i < 50; i++) {
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            mCountDownLatch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        for (int k = 0; k < 100; k++) {
                            session.selectList("CompanyMapper.getCompanysByIds", ids);
                        }
                    }
                });
                thread.start();
            }
            mCountDownLatch.countDown();
            synchronized (MybatisBugTest.class) {
                try {
                    MybatisBugTest.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }


上诉异常堆栈信息在并发环境下果然重现出现,根据异常信息代码执行至该行代码时发生异常:

Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
    at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837)


异常信息表明OgnlRuntime类不能够访问java.util.Collections的私有成员

SingletonList。查看源代码发现能够抛出 MethodFailedException 异常可以锁定在 invokeMethod 方法内部。


public static Object callAppropriateMethod(OgnlContext context, Object source, Object target, String methodName, String propertyName, List methods, Object[] args) throws MethodFailedException {
        Object reason = null;
        Object[] actualArgs = objectArrayPool.create(args.length);
        try {
            Method e = getAppropriateMethod(context, source, target, methodName, propertyName, methods, args, actualArgs);
            if(e == null || !isMethodAccessible(context, source, e, propertyName)) {
                StringBuffer buffer = new StringBuffer();
                if(args != null) {
                    int i = 0;
                    for(int ilast = args.length - 1; i <= ilast; ++i) {
                        Object arg = args[i];
                        buffer.append(arg == null?NULL_STRING:arg.getClass().getName());
                        if(i < ilast) {
                            buffer.append(", ");
                        }
                    }
                }
                throw new NoSuchMethodException(methodName + "(" + buffer + ")");
            }
            Object var14 = invokeMethod(target, e, actualArgs);
            return var14;
        } catch (NoSuchMethodException var21) {
            reason = var21;
        } catch (IllegalAccessException var22) {
            reason = var22;
        } catch (InvocationTargetException var23) {
            reason = var23.getTargetException();
        } finally {
            objectArrayPool.recycle(actualArgs);
        }
        throw new MethodFailedException(source, methodName, (Throwable)reason);
    }


invokeMethod 方法代码


public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
        boolean wasAccessible = true;
        if(securityManager != null) {
            try {
                securityManager.checkPermission(getPermission(method));
            } catch (SecurityException var6) {
                throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
            }
        }
        if((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !(wasAccessible = method.isAccessible())) {
            method.setAccessible(true); (1)
        }
        Object result = method.invoke(target, argsArray); (3)
        if(!wasAccessible) {
            method.setAccessible(false); (2)
        }
        return result;
    }


问题出现在 method 实际上是一个共享变量,也就是例子中的


public int java.util.Collections$SingletonList.size()


方法


当第一个线程 t1 至 (1) 行代码允许 method 方法可以被调用,第二个线程 t2 执行至 (2) 将 method 的方法设置为不可以访问。接着 t1 又开始执行到 (3) 行的时候就会发生该异常。这是一个很典型的同步问题。Ognl2.7 已经修复了该问题,因为 ognl 源码是直接打包内嵌在 mybatis 包中, mybatis3.3.0 版本中也已经进行了修复升级。(划重点)


public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
        boolean syncInvoke = false;
        boolean checkPermission = false;
        int mHash = method.hashCode();
        synchronized(method) {
            if(_methodAccessCache.get(Integer.valueOf(mHash)) == null || _methodAccessCache.get(Integer.valueOf(mHash)) == Boolean.TRUE) {
                syncInvoke = true;
            }
            if(_securityManager != null && _methodPermCache.get(Integer.valueOf(mHash)) == null || _methodPermCache.get(Integer.valueOf(mHash)) == Boolean.FALSE) {
                checkPermission = true;
            }
        }
        boolean wasAccessible = true;
        Object result;
        if(syncInvoke) {
            synchronized(method) {
                if(checkPermission) {
                    try {
                        _securityManager.checkPermission(getPermission(method));
                        _methodPermCache.put(Integer.valueOf(mHash), Boolean.TRUE);
                    } catch (SecurityException var12) {
                        _methodPermCache.put(Integer.valueOf(mHash), Boolean.FALSE);
                        throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
                    }
                }
                if(Modifier.isPublic(method.getModifiers()) && Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
                    _methodAccessCache.put(Integer.valueOf(mHash), Boolean.FALSE);
                } else if(!(wasAccessible = method.isAccessible())) {
                    method.setAccessible(true);
                    _methodAccessCache.put(Integer.valueOf(mHash), Boolean.TRUE);
                } else {
                    _methodAccessCache.put(Integer.valueOf(mHash), Boolean.FALSE);
                }
                result = method.invoke(target, argsArray);
                if(!wasAccessible) {
                    method.setAccessible(false);
                }
            }
        } else {
            if(checkPermission) {
                try {
                    _securityManager.checkPermission(getPermission(method));
                    _methodPermCache.put(Integer.valueOf(mHash), Boolean.TRUE);
                } catch (SecurityException var11) {
                    _methodPermCache.put(Integer.valueOf(mHash), Boolean.FALSE);
                    throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
                }
            }
            result = method.invoke(target, argsArray);
        }
        return result;
    }


相关文章
|
SQL 缓存 Java
|
2月前
|
SQL XML Java
mybatis复习01,简单配置让mybatis跑起来
文章介绍了MyBatis的基本概念、历史和特点,并详细指导了如何配置MyBatis环境,包括创建Maven项目、添加依赖、编写核心配置文件、创建数据表和实体类、编写Mapper接口和XML配置文件,以及如何编写工具类和测试用例。
mybatis复习01,简单配置让mybatis跑起来
|
SQL XML 存储
一.吃透Mybatis源码-Mybatis初始化
Mybatis是Java 项目开发使用率非常高的一款持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 同时Mybatis也是面试过程中被高频问到的一门技术,今天我就带大家一起来对Mybatis的重要原理及其源码进行一个分析。
|
6月前
|
SQL 缓存 Java
一文彻底搞懂Mybatis系列(十五)之MyBatis二级缓存
一文彻底搞懂Mybatis系列(十五)之MyBatis二级缓存
432 0
|
6月前
|
Java 关系型数据库 数据库连接
干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁
干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁
|
SQL XML Java
初始MyBatis,w字带你解MyBatis
初始MyBatis,w字带你解MyBatis
|
SQL XML Java
10 种超好用的 MyBatis 写法,同事都说好用!(2)
10 种超好用的 MyBatis 写法,同事都说好用!
|
SQL Oracle 安全
10 种超好用的 MyBatis 写法,同事都说好用!(1)
10 种超好用的 MyBatis 写法,同事都说好用!
|
SQL Java 数据库连接
mybatis plus注意的地方
1 可能你会觉得奇怪,明明我创建表的时候,设置id自增从1开始。自增主键,数据库设置了自增主键后,po类需要增加@TableId(type = IdType.AUTO),或者不要id字段才能生效。
140 0
mybatis plus注意的地方
|
SQL Oracle Java
学习mybatis-plus,这一篇就够了(一)
学习mybatis-plus,这一篇就够了
学习mybatis-plus,这一篇就够了(一)
下一篇
无影云桌面