四时宝库

程序员的知识宝库

Mybatis 有坑,千万别踩(mybatischoose)

作者:蓬蒿

来源:zhuanlan.zhihu.com/p/30085658

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表达式为空等特殊情况均不会重现该异常。

具体异常堆栈信息如下:

### 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为何还会出现不可访问的异常。该问题并不是每一次都会出现,经过多次尝试,该异常一直未在测试环境重现。

该接口在完整调用链路中的出错次数占总调用次数的比率为0.01%,无意中联想到并发问题在周期性时间内往往是概率性发生。

编写模拟多线程环境并发读取公司列表测试代码:

多线程并发环境下的压测代码

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();
????}
}

上诉异常堆栈信息在并发环境下果然重现出现,根据异常信息代码执行至该行代码时发生异常:

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。查看源代码发现能够抛出MethodFailedException异常可以锁定在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);
}

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;
}

问题出现在method实际上是一个共享变量,也就是例子中的

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;
}

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接