23

警惕 | 警惕,mybatis的size()方法竟然有坑!

 3 years ago
source link: https://www.iming.info/2020/11/03/警惕-警惕,mybatis的size方法竟然有坑!/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Hi ! 我是小小,今天我们又见面了,今日的主要内容是MyBatis的size方法使用的主要的注意事项。

前言

MyBatis 是一个开源的轻量级的半自动化的 ORM 框架,用于面向对象和关系型数据库的映射,其中 xml 文件,和sql语句结合,最大的特点,应用程序sql解耦。

OGNL表达式,是MyBatis中的广泛应用,是一种EL语言,用于设置和获取 Java 对象的属性,并且可以对列表进行投影和执行lambda表达式,ognl提供了简单,便于执行的ognl表达式。

一个线上服务,经常会出现一个异常,构造各种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.CollectionsSingletonList 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.CollectionsSingletonList 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.MybatisBugTest2.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.CollectionsSingletonList 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。查看源代码,可以知道锁定在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);
    }

其方法代码

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;
    }

问题出现在meta是一个共享变量,即

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

当,第一个线程t1到第一行代码允许method方法可以调用,第二个线程t2,执行到2把方法method设置为不可访问,接着t1又执行,此时行列3会发生异常。

升级版本

lgnl2.7,已经修复了这个问题,所以修复后的代码如下

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;
    }

关于作者

我是小小,一个生于二线,活在一线城市的程序猿,我是小小,我们下期再见。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK