记一次Mybatis并发问题分析复现
2022-06-16
阅读量
现象
测试环境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
问题就是在并发场景下,因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多线程调试模式可以方便的重现多线程问题
- 我们遇到的大多数问题,都是其他人遇到过的,遇到问题先去社区找一找