MyBatis 3.2.x版本在并发情况下可能出现的bug及解决办法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mj158518/article/details/52185949

我们基于Spring的Web项目使用的MyBatis版本是3.2.3,有一天忽然发现出现了很神奇的异常,如下:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'searchParam.numbers != null and searchParam.numbers.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [111] [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.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:75) ~[mybatis-spring-1.2.1.jar:1.2.1]
        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:368) ~[mybatis-spring-1.2.1.jar:1.2.1]
        at com.sun.proxy.$Proxy26.selectList(Unknown Source) ~[na:na]
        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198) ~[mybatis-spring-1.2.1.jar:1.2.1]
        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:114) ~[mybatis-3.2.3.jar:3.2.3]
        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:58) ~[mybatis-3.2.3.jar:3.2.3]
        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:43) ~[mybatis-3.2.3.jar:3.2.3]
        at com.sun.proxy.$Proxy55.query(Unknown Source) ~[na:na]
觉得很奇怪,因为这个size方法是public的,怎么就没法调用呢?而且并不是每次都出现,推断不是写法的问题。那问题到底出现在哪里呢?发现当处理比较频繁的时候,出现问题的概率较大(但也就每天几个十几个,平时是几天一次)。


后来同事从网上查了下,发现是OGNL的一个bug。

MyBatis 3.2.3版本使用的OGNL版本是2.6.9,该版本在并发时存在bug,如下面的测试程序:

import org.apache.ibatis.scripting.xmltags.ExpressionEvaluator;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

@RunWith(JUnit4.class)
public class OgnlConcurrentTest {

    private ExpressionEvaluator evaluator = new ExpressionEvaluator();

    @Test
    public void testConcurrent() throws InterruptedException {
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch count = new CountDownLatch(100);

        final AtomicInteger errorCount = new AtomicInteger();

        final List<String> list = new ArrayList<>();
        list.add("one");
        list.add("two");

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        start.await();
                    } catch (Exception ignored) {
                    }

                    for (int j = 0; j < 100; j++) {
                        try {
                            evaluator.evaluateBoolean("size() > 0", Collections.unmodifiableList(list));
                        } catch (Exception e) {
                            e.printStackTrace();
                            errorCount.incrementAndGet();
                        }
                    }

                    count.countDown();
                }
            }).start();
        }

        start.countDown();
        count.await();

        Assert.assertEquals(0, errorCount.get());
    }
}
程序每次运行结果不同,但基本都会报错,输出截取部分如下:

org.apache.ibatis.builder.BuilderException: Error evaluating expression 'size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [one, two] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$UnmodifiableCollection with modifiers "public"]
	at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:47)
	at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:29)
	at OgnlConcurrentTest$1.run(OgnlConcurrentTest.java:51)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [one, two] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$UnmodifiableCollection 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.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.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)
	... 3 more

java.lang.AssertionError: 
Expected :0
Actual   :42
问题出现在OgnlRuntime.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);
	}

	Object result = method.invoke(target, argsArray);
	if(!wasAccessible) {
		method.setAccessible(false);
	}

	return result;
}
上面出问题的两种List都是Collections类里面的内部类,访问修饰符都不是public(一个是private,另一个是默认),这样method.isAccessible()的结果就是false。

假设有两个线程t1和t2,t2执行到第13行的时候,t1正好执行了第17行,此时t2再执行第15行的时候,就会报错了。


OGNL在2.7版本修复了这个问题(MyBatis在3.3.x版本升级了OGNL),对这部分加上了同步,最新实现(ognl-3.1.8)如下:

public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
	boolean syncInvoke = false;
	boolean checkPermission = false;
	synchronized(method) {
		if(_methodAccessCache.get(method) == null || _methodAccessCache.get(method) == Boolean.TRUE) {
			syncInvoke = true;
		}

		if(_securityManager != null && _methodPermCache.get(method) == null || _methodPermCache.get(method) == Boolean.FALSE) {
			checkPermission = true;
		}
	}

	boolean wasAccessible = true;
	Object result;
	if(syncInvoke) {
		synchronized(method) {
			if(checkPermission) {
				try {
					_securityManager.checkPermission(getPermission(method));
					_methodPermCache.put(method, Boolean.TRUE);
				} catch (SecurityException var11) {
					_methodPermCache.put(method, Boolean.FALSE);
					throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
				}
			}

			if(Modifier.isPublic(method.getModifiers()) && Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
				_methodAccessCache.put(method, Boolean.FALSE);
			} else if(!(wasAccessible = method.isAccessible())) {
				method.setAccessible(true);
				_methodAccessCache.put(method, Boolean.TRUE);
			} else {
				_methodAccessCache.put(method, Boolean.FALSE);
			}

			result = method.invoke(target, argsArray);
			if(!wasAccessible) {
				method.setAccessible(false);
			}
		}
	} else {
		if(checkPermission) {
			try {
				_securityManager.checkPermission(getPermission(method));
				_methodPermCache.put(method, Boolean.TRUE);
			} catch (SecurityException var10) {
				_methodPermCache.put(method, Boolean.FALSE);
				throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
			}
		}

		result = method.invoke(target, argsArray);
	}

	return result;
}
代码有些长,不过主要思想就是加上了同步,剩下的就是考虑只在需要同步的时候才同步,避免影响性能。

全局有一个_methodAccessCache,保存了方法与访问权限的映射关系。当_methodAccessCache.get(method) == null时,表示是第一次遇到这个方法,此时需要同步校验;当_methodAccessCache.get(method) == Boolean.TRUE表示之前遇到过,且并不是可访问的(需要人工设置可访问,访问后再还原),此时需要同步校验;除了上面这两种情况,就不需要同步了。

最终我们是通过升级MyBatis解决的这个问题,我们将MyBatis升级到最新的3.4.1版本,同时也需要将mybatis-spring升级到1.3.0版本。


下面给出一些链接供参考:

Ognl-2.6.9 the concurrent bug and must be upgraded to more than 2.7 version

setAccessible(false) threading bug

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页