Mybatis是一個開源的輕量級半自動化ORM框架,使得面向對象應用程序與關係數據庫的映射變得更加容易。MyBatis使用xml描述符或註解將對象與存儲過程或SQL語句相結合。Mybatis最大優勢是應用程序與Sql進行解耦,sql語句是寫在Xml Mapper文件中。 OGNL表達式在Mybatis當中應用很是普遍,其表達式的靈活性使得動態Sql功能的很是強大。OGNL是Object-Graph Navigation Language的縮寫,表明對象圖導航語言。OGNL是一種EL表達式語言,用於設置和獲取Java對象的屬性,而且能夠對列表進行投影選擇以及執行lambda表達式。Ognl類提供了許多簡便方法用於執行表達式的。Struts2發佈的每一個版本都會出現的新的高危可執行漏洞也是由於它使用了靈活的OGNL表達式。 公司後端採用Mybatis做爲數據訪問層,所使用版本爲3.2.3。線上環境業務系統在運行過程當中出現了一個使人困惑的異常, 該異常時而出現時而不出現,構造各類OGNL表達式爲空等特殊狀況均不會重現該異常。具體異常堆棧信息以下:java
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.MybatisBugTest$2.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.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%,無心中聯想到併發問題在週期性時間內每每是機率性發生。 編寫模擬多線程環境併發讀取公司列表測試代碼: <select id="getCompanysByIds"resultType="cn.com.shaobingmm.Company"> select * from company and id in #{id} 多線程併發環境下的壓測代碼sql
String resource = "mybatis-config.xml"; InputStream in = null; try { in = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); final List 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)express
異常信息代表OgnlRuntime類不可以訪問java.util.Collections的私有成員SingletonList。查看源代碼發現可以拋出MethodFailedException異常能夠鎖定在invokeMethod方法內部。apache
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);
}
複製代碼
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."); } }session
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;
}
複製代碼
問題出如今method其實是一個共享變量,也就是例子中的mybatis
public int java.util.Collections$SingletonList.size() 方法 當第一個線程t1至(1)行代碼容許method方法能夠被調用,第二個線程t2執行至(2)將method的方法設置爲不能夠訪問。接着t1又開始執行到(3)行的時候就會發生該異常。這是一個很典型的同步問題。 Ognl2.7已經修復了該問題,由於ognl源碼是直接打包內嵌在mybatis包中,mybatis3.3.0版本中也已經進行了修復升級。(劃重點) 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;
}複製代碼