1. 程式人生 > >MyBatis 3.2.x版本在併發情況下可能出現的bug及解決辦法

MyBatis 3.2.x版本在併發情況下可能出現的bug及解決辦法

我們基於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版本。


下面給出一些連結供參考: