现象

测试环境SQL执行失败,有如下错误日志

Caused by: org.apache.ibatis.ognl.NoSuchPropertyException: com.xxx.entity.BusinessRequest.specialFlag
        at org.apache.ibatis.ognl.ObjectPropertyAccessor.getProperty(ObjectPropertyAccessor.java:151) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.OgnlRuntime.getProperty(OgnlRuntime.java:2420) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.ASTProperty.getValueBody(ASTProperty.java:114) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:258) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:141) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:258) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.ASTNotEq.getValueBody(ASTNotEq.java:50) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:258) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:494) ~[mybatis-3.3.1.jar:3.3.1]
        at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:458) ~[mybatis-3.3.1.jar:3.3.1]

排查过程

实体类

public class BusinessRequest {

    private String specialFlag;

    public String getSpecialFlag() {
        return specialFlag;
    }

    public void setSpecialFlag(String specialFlag) {
        this.specialFlag = specialFlag;
    }
}

DAO定义

public interface BusinessRequestDao {

    BusinessRequest select(@Param("request") BusinessRequest request);
    
}

SQL文件

<insert id="select" parameterType="com.xxx.entity.BusinessRequest" >
  select * from table where id > 0
  <if test="request.specialFlag != null" >
    and special_flag = #{request.specialFlag,jdbcType=VARCHAR}
  </if>
  limit 1
</insert>

1、Mybatis常见可能造成NoSuchPropertyException的原因

  • SQL文件中表达示的属性名写错

  • 实体类属性为private类型,但没有写public的GetSet方法(isXX、hasXX也可以)
    优先尝试通过方法(getXX、isXX、hasXX)取值,取不到的话,通过属性直接获取

  • GetSet方法格式错误

private String tFoo;

// 错误
public String getTFoo() {
    return tFoo;
}

// 正确
public String gettFoo() {
    return tFoo;
}

仔细检查了代码,排除了以上几种原因

2、去github下搜索NoSuchPropertyException关键字,有类似问题的issues

https://github.com/mybatis/mybatis-3/issues/623

问题就是在并发场景下,因OGNL的BUG导致获取不到实体类的属性而抛出异常,OGNL获取值的代码如下:

/**
* 获取 target 实例的属性名为 name 的值
*/
public Object getPossibleProperty(Map context, Object target, String name) {
    Object result;
    OgnlContext ognlContext = (OgnlContext) context;

    // 1、通过方法获取属性值
    if ((result = OgnlRuntime.getMethodValue(ognlContext, target, name, true)) == OgnlRuntime.NotFound) {
        // 2、直接反射获取值
        result = OgnlRuntime.getFieldValue(ognlContext, target, name, true);
    }
    return result;
}

/**
* 获取 target 实例的属性名为 propertyName 的值
* 如果 checkAccessAndExistence = true,无法获取属性值时,返回常量NotFound
*/
public static final Object getMethodValue(OgnlContext context, Object target, String propertyName, boolean checkAccessAndExistence) {
    Object result = null;
    // 1.1获取 propertyName 属性的get方法
    Method m = getGetMethod(context, (target == null) ? null : target.getClass() , propertyName);
    if (m == null) {
        // 1.2获取 propertyName 属性的其他read方法类似 is + name, has + name, get + name, etc..
        m = getReadMethod((target == null) ? null : target.getClass(), propertyName, 0); // ②
    }

    if (checkAccessAndExistence) {
        if ((m == null) || !context.getMemberAccess().isAccessible(context, target, m, propertyName)) {
            result = NotFound;
        }
    }
    if (result == null) {
        if (m != null) {
            result = invokeMethod(target, m, NoArguments);
        }
    }
    return result;
}


/**
*  获取 targetClass 类的属性名为 propertyName 的get方法
*/
public static Method getGetMethod(OgnlContext context, Class targetClass, String propertyName) {
    // 1.1.1 从GetMethod缓存中获取
    Method method = cacheGetMethod.get(targetClass, propertyName);
    if (method != null)
        return method;
    
    // 1.1.2 再次检查缓存,如果缓存中已经存在,返回null(为什么返回null?因为可能缓存中有值,但值为null,避免再次反射)
    if (cacheGetMethod.containsKey(targetClass, propertyName)) // ①
        return null;
    
    // 1.1.3 缓存中不存在,通过反射获取get方法,并放入缓存(无论method是否为null都放入缓存,避免下次调用时再次反射)
    method = _getGetMethod(context, targetClass, propertyName);
    cacheGetMethod.put(targetClass, propertyName, method);
    
    return method;
}

原因分析

1、并发场景下getGetMethod方法可能因为以下原因返回null

  • A线程执行到1.1.2的if语句时,暂停
  • B线程执行完1.1.3获取到method并放入缓存
  • A线程继续执行if判断,缓存中已有值,返回null

2、getMethodValue里面调用getReadMethod方法时,因为入参numParms=0,getReadMethod方法总是返回null

结果就是明明实体类有对应的属性,却获取不到,此BUG只在初次并发执行SQL时出现

但OGLN后续版本的解决方案并不是修改getGetMethod方法为线程安全,而是修改了调用getReadMethod方法的入参numParms

问题重现

  • idea在 ① 行打断点,Suspend类型设置为Thread
  • A线程运行到 ① 行时
  • 切换到B线程,B线程直接运行到结束
  • 切换到A线程,继续执行

解决方案

升级mybatis版本

总结

  • 线程安全问题很容易忽视,多做并发测试
  • idea多线程调试模式可以方便的重现多线程问题
  • 我们遇到的大多数问题,都是其他人遇到过的,遇到问题先去社区找一找