难受,被 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;
    }


相关文章
|
9月前
|
SQL XML 存储
一.吃透Mybatis源码-Mybatis初始化
Mybatis是Java 项目开发使用率非常高的一款持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 同时Mybatis也是面试过程中被高频问到的一门技术,今天我就带大家一起来对Mybatis的重要原理及其源码进行一个分析。
|
11月前
|
SQL 算法 Java
用过MyBatis-Plus,我再也不想用mybatis了——MyBatis-Plus快速入门加常见注解总结,一文快速掌握MyBatis-Plus
用过MyBatis-Plus,我再也不想用mybatis了——MyBatis-Plus快速入门加常见注解总结,一文快速掌握MyBatis-Plus
|
11月前
|
SQL XML Java
初始MyBatis,w字带你解MyBatis
初始MyBatis,w字带你解MyBatis
|
SQL Java 数据库连接
mybatis plus注意的地方
1 可能你会觉得奇怪,明明我创建表的时候,设置id自增从1开始。自增主键,数据库设置了自增主键后,po类需要增加@TableId(type = IdType.AUTO),或者不要id字段才能生效。
113 0
mybatis plus注意的地方
|
SQL 数据库
学习mybatis-plus,这一篇就够了(二)
学习mybatis-plus,这一篇就够了
学习mybatis-plus,这一篇就够了(二)
|
SQL Oracle Java
学习mybatis-plus,这一篇就够了(一)
学习mybatis-plus,这一篇就够了
学习mybatis-plus,这一篇就够了(一)
|
SQL 测试技术 数据库
学习mybatis-plus,这一篇就够了(四)
学习mybatis-plus,这一篇就够了
学习mybatis-plus,这一篇就够了(四)
|
SQL 缓存 Java
2022最新最全MyBatis(简单全面,一发入魂,内容超详细)
一、MyBatis简介 1、MyBatis历史 2、MyBatis特性 3、MyBatis下载 4、和其它持久化层技术对比 JDBC Hibernate 和 JPA MyBatis 二、搭建MyBatis 1、开发环境 2、创建maven工程 a&gt;打包方式:jar b&gt;引入依赖 3、创建MyBatis的核心配置文件 4、创建mapper接口 5、创建MyBatis的映射文件 6、通过junit测试功能 7、加入log4j日志功能 a&gt;加入依赖 b&gt;加入log4j的配置文件 三、核心配置文件详解 四、MyBatis的增删改查 五、MyBatis获取参数
2022最新最全MyBatis(简单全面,一发入魂,内容超详细)
|
缓存 算法 安全
Mybatis从小白到小黑(七)Mybatis缓存详解
在 Web 系统中,最重要的操作就是查询数据库中的数据。但是有些时候查询数据的频率非常高,这是很耗费数据库资源的,往往会导致数据库查询效率极低,影响客户的操作体验。于是我们可以将一些变动不大且访问频率高的数据,放置在一个缓存容器中,用户下一次查询时就从缓存容器中获取结果。