警惕,MyBatis的size()方法竟然有坑!
來源:http://h5ip.cn/aJgJ
Mybatis是一個(gè)開源的輕量級半自動(dòng)化ORM框架,使得面向?qū)ο髴?yīng)用程序與關(guān)系數(shù)據(jù)庫的映射變得更加容易。MyBatis使用xml描述符或注解將對象與存儲過程或SQL語句相結(jié)合。Mybatis最大優(yōu)點(diǎn)是應(yīng)用程序與Sql進(jìn)行解耦,sql語句是寫在Xml Mapper文件中。OGNL表達(dá)式在Mybatis當(dāng)中應(yīng)用非常廣泛,其表達(dá)式的靈活性使得動(dòng)態(tài)Sql功能的非常強(qiáng)大。OGNL是Object-Graph Navigation Language的縮寫,代表對象圖導(dǎo)航語言。OGNL是一種EL表達(dá)式語言,用于設(shè)置和獲取Java對象的屬性,并且可以對列表進(jìn)行投影選擇以及執(zhí)行l(wèi)ambda表達(dá)式。Ognl類提供了許多簡便方法用于執(zhí)行表達(dá)式的。Struts2發(fā)布的每個(gè)版本都會出現(xiàn)的新的高??蓤?zhí)行漏洞也是因?yàn)樗褂昧遂`活的OGNL表達(dá)式。公司后端采用Mybatis作為數(shù)據(jù)訪問層,所使用版本為3.2.3。線上環(huán)境業(yè)務(wù)系統(tǒng)在運(yùn)行過程中出現(xiàn)了一個(gè)令人困惑的異常, 該異常時(shí)而出現(xiàn)時(shí)而不出現(xiàn),構(gòu)造各種OGNL表達(dá)式為空等特殊情況均不會重現(xiàn)該異常。具體異常堆棧信息如下:
- ### Error querying database. Cause: 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.Collections$SingletonList with modifiers "public"]
- ### Cause: 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.Collections$SingletonList with modifiers "public"]
- 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.Collections$SingletonList with modifiers "public"]
- at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java
- at:47)
- at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:29)
- at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:30)
- at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
- at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:51)
- at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
- at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:37)
- at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:275)
- at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:79)
- at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:104)
- ... 3 more
- 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)
- 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為何還會出現(xiàn)不可訪問的異常。該問題并不是每一次都會出現(xiàn),經(jīng)過多次嘗試,該異常一直未在測試環(huán)境重現(xiàn)。該接口在完整調(diào)用鏈路中的出錯(cuò)次數(shù)占總調(diào)用次數(shù)的比率為0.01%,無意中聯(lián)想到并發(fā)問題在周期性時(shí)間內(nèi)往往是概率性發(fā)生。編寫模擬多線程環(huán)境并發(fā)讀取公司列表測試代碼:
- <mapper namespace="CompanyMapper">
- <select id="getCompanysByIds"resultType="cn.com.shaobingmm.Company">
- select *
- from company
- <where>
- <if test="list != null and list.size() > 0">
- and id in
- <foreach collection="list" item="id" open="(" separator="," close=")">#{id}
- </foreach>
- </if>
- </where>
- </select>
- </mapper>
多線程并發(fā)環(huán)境下的壓測代碼
- String resource = "mybatis-config.xml";
- InputStream in = null;
- try {
- in = Resources.getResourceAsStream(resource);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
- final List<Long> 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();
- }
- }
上訴異常堆棧信息在并發(fā)環(huán)境下果然重現(xiàn)出現(xiàn),根據(jù)異常信息代碼執(zhí)行至該行代碼時(shí)發(fā)生異常:
- 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)
異常信息表明OgnlRuntime類不能夠訪問java.util.Collections的私有成員SingletonList。查看源代碼發(fā)現(xiàn)能夠拋出MethodFailedException異??梢枣i定在invokeMethod方法內(nèi)部。
- 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.");
- }
- }
- 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;
- }
問題出現(xiàn)在method實(shí)際上是一個(gè)共享變量,也就是例子中的
- public int java.util.Collections$SingletonList.size()
方法
當(dāng)?shù)谝粋€(gè)線程t1至(1)行代碼允許method方法可以被調(diào)用,第二個(gè)線程t2執(zhí)行至(2)將method的方法設(shè)置為不可以訪問。接著t1又開始執(zhí)行到(3)行的時(shí)候就會發(fā)生該異常。這是一個(gè)很典型的同步問題。Ognl2.7已經(jīng)修復(fù)了該問題,因?yàn)閛gnl源碼是直接打包內(nèi)嵌在mybatis包中,mybatis3.3.0版本中也已經(jīng)進(jìn)行了修復(fù)升級。(劃重點(diǎn))
- 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;
- }