起因
有位同事写mybatis的Mapper接口文件时使用@param传递参数。大致的写法 我就用伪代码意思一下,大家意会即可。
//接口
void getList(@Param("user") UserParam user);
--sql
<select resultMap="userResultMap">
select id,name,age
from user
<where>
<if test = "user.NAME != null and user.name!= '' ">
and name = #{user.name}
</if>
</where>
</select>
mybatis的if标签中为了获取name值,使用了不同的大小写方式,但最终都成功获取到了name值。于是我就想看看mybatis是如何获取的,是不是忽略了大小写呢?如果实体中有getName()和getnaMe()两个大小写不同的方法,最终会选择哪个方法呢?
源码
涉及到的源码主要在(org.apache.ibatis.ognl.OgnlRuntime)。我在贴出的代码中进行注释。
1.getMethodValue()
主要作用根据Mapper文件的字段值(user.NAME等) 从user中获取对应的值。
/**
* Object target = 传入的对象user
* String propertyName = Mapper文件中的NAME
* 该方法被循环调用,通过Mapper文件中的字段值在对象user中获取值。
* https://blog.csdn.net/qq_45044391/article/details/119146614
*/
public static final Object getMethodValue(OgnlContext context, Object target, String propertyName, boolean checkAccessAndExistence) throws OgnlException, IllegalAccessException, NoSuchMethodException, IntrospectionException {
Object result = null;
//getGetMethod方法是通过反射获取对象user的内部方法,然后比较字符串比较获取对应的方法
/*getGetMethod 不展开了,主要判断已经贴在这里了
String ms = methods[i].getName();
if (ms.endsWith(baseName)) baseName即传入的字段名"NAME"
if ((isSet = ms.startsWith("set")) || ms.startsWith("get") || (isIs = ms.startsWith("is")))
int prefixLength = isIs ? 2 : 3;
if (isSet == false && baseName.length() == ms.length() - prefixLength)
如果方法以 get/is开头 以baseName结尾,同时两者之间没有其他字符,则放入list集合中。
然后循环集合判断方法的参数,如果参数个数为0,则返回该方法
*/
Method m = getGetMethod(context, target == null ? null : target.getClass(), propertyName);
//由于我写的user.NAME在对象中没有getNAME()的方法,所以这里m为空
if (m == null) {
//getReadMethod()源码已经贴在下面
m = getReadMethod(target == null ? null : target.getClass(), propertyName, (Class[])null);
}
if (checkAccessAndExistence && (m == null || !context.getMemberAccess().isAccessible(context, target, m, propertyName))) {
//public static final Object NotFound = new Object();
//如果找不到对应的方法 返回一个new Object()
result = NotFound;
}
if (result == null) {
if (m == null) {
throw new NoSuchMethodException(propertyName);
}
try {
//执行获取的方法 m ,返回user中的值
result = invokeMethod(target, m, NoArguments);
} catch (InvocationTargetException var7) {
throw new OgnlException(propertyName, var7.getTargetException());
}
}
return result;
}
2.getReadMethod()
当简单的字符串匹配无法从user对象获取对应的get方法,则执行该方法再次尝试获取方法
/**
* Class target = user的class 即UserParam.class
* String name = "NAME"
* Class[] argClasses = null
* https://blog.csdn.net/qq_45044391/article/details/119146614
*/
public static Method getReadMethod(Class target, String name, Class[] argClasses) {
try {
//处理可能存在的特殊字符
if (name.indexOf(34) >= 0) {
name = name.replaceAll("\"", "");
}
//将传入的字段不管三七二十一,直接转化为小写
name = name.toLowerCase();
//通过Introspector内省的方式获取Class对象及其父类 所有公开的方法
BeanInfo info = Introspector.getBeanInfo(target);
MethodDescriptor[] methods = info.getMethodDescriptors();
ArrayList<Method> candidates = new ArrayList();
//将获取的方法循环调用
int reqArgCount;
for(reqArgCount = 0; reqArgCount < methods.length; ++reqArgCount) {
//我会将每一步判断的大致含义注释出来
if (//(!isJdk15() || !m.isSynthetic()) && !Modifier.isVolatile(m.getModifiers())
isMethodCallable(methods[reqArgCount].getMethod())
//方法名与传入字段必须有一定的联系
&& (//忽略大小写的情况下 方法名和传入的字段值是否相同
methods[reqArgCount].getName().equalsIgnoreCase(name)
|| methods[reqArgCount].getName().toLowerCase().equals("get" + name)
|| methods[reqArgCount].getName().toLowerCase().equals("has" + name)
|| methods[reqArgCount].getName().toLowerCase().equals("is" + name)
)
//方法名不能以set开头
&& !methods[reqArgCount].getName().startsWith("set")) {
//将符合条件的方法保存起来
candidates.add(methods[reqArgCount].getMethod());
}
}
OgnlRuntime.MatchingMethod mm;
if (!candidates.isEmpty()) {
//从符合条件的方法中挑选一个最好的返回回去。
//findBestMethod()源码已经贴出来
mm = findBestMethod(candidates, target, name, argClasses);
if (mm != null) {
//正常情况下 走到这里已经能拿到需要的值
return mm.mMethod;
}
}
for(reqArgCount = 0; reqArgCount < methods.length; ++reqArgCount) {
//走到这一步说明,上面的筛选条件还是太严格,没有找到可用的方法
//于是决定将方法再次筛选,将第一次没有选上的方法放宽条件再次筛选
if (isMethodCallable(methods[reqArgCount].getMethod())
&& methods[reqArgCount].getName().equalsIgnoreCase(name)
&& !methods[reqArgCount].getName().startsWith("set")
&& !methods[reqArgCount].getName().startsWith("get")
&& !methods[reqArgCount].getName().startsWith("is")
&& !methods[reqArgCount].getName().startsWith("has")
&& methods[reqArgCount].getMethod().getReturnType() != Void.TYPE) {
Method m = methods[reqArgCount].getMethod();
if (!candidates.contains(m)) {
candidates.add(m);
}
}
}
if (!candidates.isEmpty()) {
//从符合条件的方法中挑选一个最好的返回回去。
//findBestMethod()源码已经贴出来
mm = findBestMethod(candidates, target, name, argClasses);
if (mm != null) {
return mm.mMethod;
}
}
if (!name.startsWith("get")) {
//第二次筛选还是没有选出来,于是决定在 字段值前加上get,再次调用本方法,尝试获取
Method ret = getReadMethod(target, "get" + name, argClasses);
if (ret != null) {
return ret;
}
}
//走到这一步 已经是破罐子破摔了
if (!candidates.isEmpty()) {
reqArgCount = argClasses == null ? 0 : argClasses.length;
Iterator i$ = candidates.iterator();
while(i$.hasNext()) {
Method m = (Method)i$.next();
//不管传入参数的类型,顺序是否相同
//只要该方法的传入参数的个数 与argClasses个数相同 那就拿来用
if (m.getParameterTypes().length == reqArgCount) {
return m;
}
}
}
return null;
} catch (Throwable var9) {
throw OgnlOps.castToRuntime(var9);
}
}
3.findBestMethod()
该方法的作用从字面就能看出来,就是从筛选出来的方法中找一个“最好”的返回回去。至于什么叫“最好”,我们在往下看。
/**
* List methods 即筛选后的方法集合
* Class typeClass = user的class 即UserParam.class
* String name = "NAME".toLowerCase() 即 "name"
* Class[] argClasses = null
* https://blog.csdn.net/qq_45044391/article/details/119146614
*/
private static OgnlRuntime.MatchingMethod findBestMethod(List methods, Class typeClass, String name, Class[] argClasses) {
OgnlRuntime.MatchingMethod mm = null;
IllegalArgumentException failure = null;
int i = 0;
//上来就是一个循环 简单粗暴
for(int icount = methods.size(); i < icount; ++i) {
Method m = (Method)methods.get(i);
//获取方法的传入参数 PS:这个方法也挺长的,但是与讨论的问题关系不大,明白方法目的即可
Class[] mParameterTypes = findParameterTypes(typeClass, m);
//返回一个保存分数的对象 score=0
OgnlRuntime.ArgsCompatbilityReport report = areArgsCompatible(argClasses, mParameterTypes, m);
if (report != null) {
String methodName = m.getName();
int score = report.score;
if (!name.equals(methodName)) {
if (name.equalsIgnoreCase(methodName)) {
//忽略大小写的时候,如果方法名与传入参数相等 则得分200
score += 200;
} else if (methodName.toLowerCase().endsWith(name.toLowerCase())) {
//忽略大小写的时候,如果方法名以传入参数结尾 则得分500
score += 500;
} else {
score += 5000;
}
}
/*下面这部分 乍一看有点复杂 其实很简单
主要三种情况
1.我比你大
2.我跟你一样大
3.我比你小
*/
//当前分数大于等于 之前分数最小的方法(之后以mm代替)
if (mm != null && mm.score <= score) {
//两个分数相等时
if (mm.score == score) {
//判断两个方法的参数类型和方法名是否都相等
if (Arrays.equals(mm.mMethod.getParameterTypes(), m.getParameterTypes())
&& mm.mMethod.getName().equals(m.getName())) {
//返回类型是否相等
boolean retsAreEqual = mm.mMethod.getReturnType().equals(m.getReturnType());
//判断mm的Class是不是当前方法Class的父类 PS:getReadMethod()获取方法时包含父类的方法
if (mm.mMethod.getDeclaringClass().isAssignableFrom(m.getDeclaringClass())) {
if (!retsAreEqual && !mm.mMethod.getReturnType().isAssignableFrom(m.getReturnType())) {
System.err.println("Two methods with same method signature but return types conflict? \"" + mm.mMethod + "\" and \"" + m + "\" please report!");
}
//如果是父类 则覆盖 子类的方法优先
mm = new OgnlRuntime.MatchingMethod(m, score, report, mParameterTypes);
failure = null;
} else if (!m.getDeclaringClass().isAssignableFrom(mm.mMethod.getDeclaringClass())) {
System.err.println("Two methods with same method signature but not providing classes assignable? \"" + mm.mMethod + "\" and \"" + m + "\" please report!");
} else if (!retsAreEqual && !m.getReturnType().isAssignableFrom(mm.mMethod.getReturnType())) {
System.err.println("Two methods with same method signature but return types conflict? \"" + mm.mMethod + "\" and \"" + m + "\" please report!");
}
}
//getReadMethod()中对方法进行过滤时就已经将 isJdk15() 过滤掉了,这里不用看
else if (isJdk15() && (m.isVarArgs() || mm.mMethod.isVarArgs())) {
if (!m.isVarArgs() || mm.mMethod.isVarArgs()) {
if (!m.isVarArgs() && mm.mMethod.isVarArgs()) {
mm = new OgnlRuntime.MatchingMethod(m, score, report, mParameterTypes);
failure = null;
} else {
System.err.println("Two vararg methods with same score(" + score + "): \"" + mm.mMethod + "\" and \"" + m + "\" please report!");
}
}
} else {
int scoreCurr = 0;
int scoreOther = 0;
//argClasses=null 所以这里会抛出异常
//nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'user.NAME != null and user.name!= '''
for(int j = 0; j < argClasses.length; ++j) {
Class argClass = argClasses[j];
Class mcClass = mm.mParameterTypes[j];
Class moClass = mParameterTypes[j];
if (argClass == null) {
if (mcClass != moClass) {
if (mcClass.isAssignableFrom(moClass)) {
scoreOther += 1000;
} else if (moClass.isAssignableFrom(moClass)) {
scoreCurr += 1000;
} else {
failure = new IllegalArgumentException("Can't decide wich method to use: \"" + mm.mMethod + "\" or \"" + m + "\"");
}
}
} else if (mcClass != moClass) {
if (mcClass == argClass) {
scoreOther += 100;
} else if (moClass == argClass) {
scoreCurr += 100;
} else {
failure = new IllegalArgumentException("Can't decide wich method to use: \"" + mm.mMethod + "\" or \"" + m + "\"");
}
}
}
if (scoreCurr == scoreOther) {
if (failure == null) {
System.err.println("Two methods with same score(" + score + "): \"" + mm.mMethod + "\" and \"" + m + "\" please report!");
}
} else if (scoreCurr > scoreOther) {
mm = new OgnlRuntime.MatchingMethod(m, score, report, mParameterTypes);
failure = null;
}
}
}
} else {
//当前分数小于mm的分数 覆盖
mm = new OgnlRuntime.MatchingMethod(m, score, report, mParameterTypes);
failure = null;
}
}
}
if (failure != null) {
throw failure;
} else {
return mm;
}
总结
mybatis通过传入字段名获取实体的值 分为一下几步:
- 先通过mapper.xml文件中的字段名去匹配实体中以字段名结尾的方法,匹配规则比较严格。例如 NAME 可以匹配getNAME方法。
- 如果步骤1获取不到,则通过getReadMethod 获取实体的所有方法(包括父类的方法),将这些方法按照一定的规则筛选。
- 将筛选出来的方法传入findBestMethod ,将每个方法进行打分,取分数最小的那个作为最优解返回。
如果方法名与字段名一样(忽略大小写),则给200分;
如果方法以字段名结尾(忽略大小写),则给500分;
其他方法,则给5000分; - 根据获取到的方法取得对应的值。
如果实在找不到符合的方法就会抛出异常。
像极了改作业的老师,在乱七八糟的作业本上拼命的找答案写在哪里。最终忍无可忍 把作业本“啪”的一声糊在菜鸟程序员的脸上。
禁止转载
原文链接 https://blog.csdn.net/qq_45044391/article/details/119146614