MyBatis 3.2.x版本在併發情況下可能出現的bug及解決辦法
阿新 • • 發佈:2019-01-31
我們基於Spring的Web專案使用的MyBatis版本是3.2.3,有一天忽然發現出現了很神奇的異常,如下:
覺得很奇怪,因為這個size方法是public的,怎麼就沒法呼叫呢?而且並不是每次都出現,推斷不是寫法的問題。那問題到底出現在哪裡呢?發現當處理比較頻繁的時候,出現問題的概率較大(但也就每天幾個十幾個,平時是幾天一次)。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]
後來同事從網上查了下,發現是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()); } }
問題出現在OgnlRuntime.invokeMethod方法的實現上,該方法如下: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
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版本。
下面給出一些連結供參考: