我们基于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