难受,被 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
|
SQL XML 存储
一.吃透Mybatis源码-Mybatis初始化
Mybatis是Java 项目开发使用率非常高的一款持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 同时Mybatis也是面试过程中被高频问到的一门技术,今天我就带大家一起来对Mybatis的重要原理及其源码进行一个分析。
|
7月前
|
Java 关系型数据库 MySQL
一文彻底搞懂Mybatis系列(十七)之MyBatis使用分页插件PageHelper
一文彻底搞懂Mybatis系列(十七)之MyBatis使用分页插件PageHelper
|
7月前
|
Java 关系型数据库 数据库连接
干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁
干翻Mybatis源码系列之第十二篇:基于Mybatis Plugins做一个乐观锁
|
7月前
|
存储 SQL Java
干翻Mybatis源码系列之第十二篇:自写Mybatis拦截器实现分页操作
干翻Mybatis源码系列之第十二篇:自写Mybatis拦截器实现分页操作
|
SQL XML Java
初始MyBatis,w字带你解MyBatis
初始MyBatis,w字带你解MyBatis
|
Java API 数据库
mybatis-plus小技能:代码生成器(增加Mapper注解和Fill填充)
背景:为了提升开发效率,利用mybatisplus API读取数据库表结构生成对应的实体entity、服务service,通过模板生成映射mapper。 代码生成器完善: 增加Mapper注解和Fill填充
1163 1
|
SQL Java 数据库连接
mybatis plus注意的地方
1 可能你会觉得奇怪,明明我创建表的时候,设置id自增从1开始。自增主键,数据库设置了自增主键后,po类需要增加@TableId(type = IdType.AUTO),或者不要id字段才能生效。
149 0
mybatis plus注意的地方
|
XML Java 数据库连接
Mybatis学习笔记(1)——第一个程序
暑期之前就有打算学习SSM,但是我记得当时再配置一个框架疯狂报错,弄得我很难受,,再加上当时有点其他事情,所以就放了下来。现在很有需求要会ssm,所以就学了一下。感觉框架这东西配置就烦的要死。错一丁点就全错。。下面开始说配置详情。
114 0
Mybatis学习笔记(1)——第一个程序
|
缓存 算法 安全
Mybatis从小白到小黑(七)Mybatis缓存详解
在 Web 系统中,最重要的操作就是查询数据库中的数据。但是有些时候查询数据的频率非常高,这是很耗费数据库资源的,往往会导致数据库查询效率极低,影响客户的操作体验。于是我们可以将一些变动不大且访问频率高的数据,放置在一个缓存容器中,用户下一次查询时就从缓存容器中获取结果。