mybatis(3.4.6)源码解析get方法获取对象属性

起因

有位同事写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通过传入字段名获取实体的值 分为一下几步:

  1. 先通过mapper.xml文件中的字段名去匹配实体中以字段名结尾的方法,匹配规则比较严格。例如 NAME 可以匹配getNAME方法。
  2. 如果步骤1获取不到,则通过getReadMethod 获取实体的所有方法(包括父类的方法),将这些方法按照一定的规则筛选。
  3. 将筛选出来的方法传入findBestMethod ,将每个方法进行打分,取分数最小的那个作为最优解返回。
    如果方法名与字段名一样(忽略大小写),则给200分;
    如果方法以字段名结尾(忽略大小写),则给500分;
    其他方法,则给5000分;
  4. 根据获取到的方法取得对应的值。

如果实在找不到符合的方法就会抛出异常。
像极了改作业的老师,在乱七八糟的作业本上拼命的找答案写在哪里。最终忍无可忍 把作业本“啪”的一声糊在菜鸟程序员的脸上。

禁止转载
原文链接 https://blog.csdn.net/qq_45044391/article/details/119146614

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值