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

关于作者

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