警惕,Mybatis的Size()方法竟然有坑!
本文轉(zhuǎn)載自微信公眾號(hào)「小明菜市場(chǎng)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系小明菜市場(chǎng)公眾號(hào)。
Hi ! 我是小小,今天我們又見面了,今日的主要內(nèi)容是MyBatis的size方法使用的主要的注意事項(xiàng)。
前言
MyBatis 是一個(gè)開源的輕量級(jí)的半自動(dòng)化的 ORM 框架,用于面向?qū)ο蠛完P(guān)系型數(shù)據(jù)庫(kù)的映射,其中 xml 文件,和sql語(yǔ)句結(jié)合,最大的特點(diǎn),應(yīng)用程序sql解耦。OGNL表達(dá)式,是MyBatis中的廣泛應(yīng)用,是一種EL語(yǔ)言,用于設(shè)置和獲取 Java 對(duì)象的屬性,并且可以對(duì)列表進(jìn)行投影和執(zhí)行l(wèi)ambda表達(dá)式,ognl提供了簡(jiǎn)單,便于執(zhí)行的ognl表達(dá)式。一個(gè)線上服務(wù),經(jīng)常會(huì)出現(xiàn)一個(gè)異常,構(gòu)造各種OGNL表達(dá)式為空的情況都會(huì)重現(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,還不可訪問(wèn),該異常在測(cè)試環(huán)境未重現(xiàn),但是在接口的完整調(diào)用鏈路中出錯(cuò)的次數(shù)占總的調(diào)用次數(shù)的0.01%,這是概率性事件。
模擬測(cè)試
編寫模擬多線程并發(fā)讀取公司列表的測(cè)試代碼
- <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>
 
多線程下進(jìn)行壓力測(cè)試
- 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ā)的時(shí)候會(huì)出現(xiàn)異常。
- 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類不能訪問(wèn)
查看源碼,破案
java.util.Collections的私有成員SingletonList。查看源代碼,可以知道鎖定在invokeMethod方法上。
- 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);
 - }
 
其方法代碼
- 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;
 - }
 
問(wèn)題出現(xiàn)在meta是一個(gè)共享變量,即
- public int java.util.Collections$SingletonList.size()
 
當(dāng),第一個(gè)線程t1到第一行代碼允許method方法可以調(diào)用,第二個(gè)線程t2,執(zhí)行到2把方法method設(shè)置為不可訪問(wèn),接著t1又執(zhí)行,此時(shí)行列3會(huì)發(fā)生異常。
升級(jí)版本
lgnl2.7,已經(jīng)修復(fù)了這個(gè)問(wèn)題,所以修復(fù)后的代碼如下
- 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;
 - }
 
關(guān)于作者
我是小小,一個(gè)生于二線,活在一線城市的程序猿,我是小小,我們下期再見。















 
 
 








 
 
 
 