首先我们拿出之前的代码,在如图位置打上断点,开始调试

我们规定了一个mapper接口,而调用了mapper接口的getEmpByIdAndLastName,我们并没有实现这个接口,这是因为Mybatis会为这个接口创建一个代理对象,最终都是代理对象去调用实现。

在执行这个方法之前,代码会先来到MapperProxy.class中的invoke方法

  1. public class MapperProxy<T> implements InvocationHandler, Serializable  

我们先来看看MapperProxy这个类,它实现了InvocationHandler这个接口,这个接口就是为了实现动态代理,使用这个接口,就必须实现invoke方法,

  1. public Object invoke(Object proxy, Method method, Object[] args)   throws Throwable  
  2.   {  
  3.     try  
  4.     {  
  5.       if (Object.class.equals(method.getDeclaringClass())) {  
  6.         return method.invoke(this, args);  
  7.       }  
  8.       if (isDefaultMethod(method)) {  
  9.         return invokeDefaultMethod(proxy, method, args);  
  10.       }  
  11.     }  
  12.     catch (Throwable t)  
  13.     {  
  14.       throw ExceptionUtil.unwrapThrowable(t);  
  15.     }  
  16.     MapperMethod mapperMethod = cachedMapperMethod(method);  
  17.     return mapperMethod.execute(this.sqlSession, args);  
  18.   }  

我们当前执行的方法是getEmpByIdAndLastName,此方法第二个参数,就是这个。

接来分析第5行:

这个代理对象是由我们EmployeeMapper接口创建的,因此是用有该接口的方法,但同时也拥有Object类中的方法,我们无需对这些方法进行增强,因此放行,这就是第一个if的作用。如果不是这种Object下的方法我们会来到第16行,先将这个方法包装成MapperMethod,然后执行17行代码,我们可以尝试在17行添加上断点,这是进入方法之前的参数为

  1. public Object execute(SqlSession sqlSession, Object[] args) {  
  2.     Object result;  
  3.     switch (command.getType()) {  
  4.       case INSERT: {  
  5.       Object param = method.convertArgsToSqlCommandParam(args);  
  6.         result = rowCountResult(sqlSession.insert(command.getName(), param));  
  7.         break;  
  8.       }  
  9.       case UPDATE: {  
  10.         Object param = method.convertArgsToSqlCommandParam(args);  
  11.         result = rowCountResult(sqlSession.update(command.getName(), param));  
  12.         break;  
  13.       }  
  14.       case DELETE: {  
  15.         Object param = method.convertArgsToSqlCommandParam(args);  
  16.         result = rowCountResult(sqlSession.delete(command.getName(), param));  
  17.         break;  
  18.       }  
  19.       case SELECT:  
  20.         if (method.returnsVoid() && method.hasResultHandler()) {  
  21.           executeWithResultHandler(sqlSession, args);  
  22.           result = null;  
  23.         } else if (method.returnsMany()) {  
  24.           result = executeForMany(sqlSession, args);  
  25.         } else if (method.returnsMap()) {  
  26.           result = executeForMap(sqlSession, args);  
  27.         } else if (method.returnsCursor()) {  
  28.           result = executeForCursor(sqlSession, args);  
  29.         } else {  
  30.           Object param = method.convertArgsToSqlCommandParam(args);  
  31.           result = sqlSession.selectOne(command.getName(), param);  
  32.         }  
  33.         break;  
  34.       case FLUSH:  
  35.         result = sqlSession.flushStatements();  
  36.         break;  
  37.       default:  
  38.         throw new BindingException("Unknown execution method for: " + command.getName());  
  39.     }  
  40.     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {  
  41.       throw new BindingException("Mapper method '" + command.getName()   
  42.           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");  
  43.     }  
  44.     return result;  
  45.   } 

在执行之前这个方法做出了一个判断,当是插入时做何动作,当时更新时做了何动作,增删改都相同,每个在调用之前,都会调用method.convertArgsToSqlCommandParam(args);先将我们传入的参数转化为SQL命令可用的参数,返回一个Object对象,接着看一下select,我们属于最后一个,返回单个对象,它也同样的执行method.convertArgsToSqlCommandParam(args);接下来调用的依旧是底层的sqlSession.selectOne也就是我们第一章所提到的查找的方案一,只不过这里的param是我们传递方法中的几个零散的参数转换过来的,而第一种方法是我们直接写的,接下来看看这个转换是如何实现的:

  1. public Object convertArgsToSqlCommandParam(Object[] args) {  
  2.   return paramNameResolver.getNamedParams(args);  
  3. }  

发现该方法调用了paramNameResolver下的getNamedParams方法,我们再观察一下这个方法是做什么的:

  1. public Object getNamedParams(Object[] args) {  
  2.     final int paramCount = names.size();  
  3.     if (args == null || paramCount == 0) {  
  4.       return null;  
  5.     } else if (!hasParamAnnotation && paramCount == 1) {  
  6.       return args[names.firstKey()];  
  7.     } else {  
  8.       final Map<String, Object> param = new ParamMap<Object>();  
  9.       int i = 0;  
  10.       for (Map.Entry<Integer, String> entry : names.entrySet()) {  
  11.         param.put(entry.getValue(), args[entry.getKey()]);  
  12.         // add generic param names (param1, param2, ...)  
  13.         final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);  
  14.         // ensure not to overwrite parameter named with @Param  
  15.         if (!names.containsValue(genericParamName)) {  
  16.           param.put(genericParamName, args[entry.getKey()]);  
  17.         }  
  18.         i++;  
  19.       }  
  20.       return param;  
  21.     }  

这里我们发现,这就是为什么我们在多个参数时,不做任何修改,就在映射文件写上属性会出错的原因,因为这里默认给的就是param1、param2…..(观察第10行的for循环)

我们着重分析一下这个方法:

name:

    final int paramCount = names.size()

发现name中已经有值了,正是我们两个参数,那它是如何获取到这个值呢?在这句上我们再添加断点,

  1. private final SortedMap<Integer, String> names;  
  2.     
  3. private boolean hasParamAnnotation;  
  4.     
  5. public ParamNameResolver(Configuration config, Method method) {  
  6.   final Class<?>[] paramTypes = method.getParameterTypes();  
  7.   final Annotation[][] paramAnnotations = method.getParameterAnnotations();  
  8.   final SortedMap<Integer, String> map = new TreeMap<Integer, String>();  
  9.   int paramCount = paramAnnotations.length;  
  10.   // get names from @Param annotations  
  11.   for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {  
  12.     if (isSpecialParameter(paramTypes[paramIndex])) {  
  13.       // skip special parameters  
  14.       continue;  
  15.     }  
  16.     String name = null;  
  17.     for (Annotation annotation : paramAnnotations[paramIndex]) {  
  18.       if (annotation instanceof Param) {  
  19.         hasParamAnnotation = true;  
  20.         name = ((Param) annotation).value();  
  21.         break;  
  22.       }  
  23.     }  
  24.     if (name == null) {  
  25.       // @Param was not specified.  
  26.       if (config.isUseActualParamName()) {  
  27.         name = getActualParamName(method, paramIndex);  
  28.       }  
  29.       if (name == null) {  
  30.         // use the parameter index as the name ("0", "1", ...)  
  31.         // gcode issue #71  
  32.         name = String.valueOf(map.size());  
  33.       }  
  34.     }  
  35.     map.put(paramIndex, name);  
  36.   }  
  37.   names = Collections.unmodifiableSortedMap(map);  
  38. }  

在6、7行上它拿到了所有的参数和参数注解,在第11行开始标注参数索引,在第18行上有个很关键的动作,如果当前参数的注解是Param,那么hasParamAnnotation标位true,并且把值放在name中。如果没有标注,那么它将会选择(29行)使用map的长度作为name,而map是会随参数的增加而增加长度的。接下来最后(35行)保存了key:索引和value:name值,很显然,如果我们有Param的注解,这个name自然而然保存的是属性名,如果没有,会有两种操作,一种是看看配置文件中setting属性的isUseActualParamName属性是否为true(26行),当然如果是true,效果和我们使用Param注解将会是一个效果,这个必须在JAVA8之后使用,如果这个属性也没填写true,那么就使用当前map的长度,这样{0=id,1=lastName}就诞生了,如果我们此时有第三个参数我们没有写注解同时也没配置isUseActualParamName属性,那么他的key=2,值也将会是2 value=2,

回到getNamedParams上,看第3行,如果参数为空,直接返回,第二个判断(第5行),如果参数只有一个,并且没有Param注解,我们将会拿到map(names)第一个参数,但此时我们map(names)其实只有一个参数,

接下来的else(第7行)就是我们多参传值,通过names,而这个names在构造器阶段就已经创建好了

  1.   public Object getNamedParams(Object[] args) {  
  2.     final int paramCount = names.size();  
  3.     if (args == null || paramCount == 0) {  
  4.       return null;  
  5.     } else if (!hasParamAnnotation && paramCount == 1) {  
  6.       return args[names.firstKey()];  
  7.     } else {  
  8.       final Map<String, Object> param = new ParamMap<Object>();  
  9.       int i = 0;  
  10.       for (Map.Entry<Integer, String> entry : names.entrySet()) {  
  11.         param.put(entry.getValue(), args[entry.getKey()]);  
  12.         // add generic param names (param1, param2, ...)  
  13.         final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);  
  14.         // ensure not to overwrite parameter named with @Param  
  15.         if (!names.containsValue(genericParamName)) {  
  16.           param.put(genericParamName, args[entry.getKey()]);  
  17.         }  
  18.         i++;  
  19.       }  
  20.       return param;  
  21.     }  
  22.   }  
  23. }  

遍历names集合,将每一个值放入param这个map中(第11行),它将names中的values作为key,而将args,就是我们最外面那层getEmpByIdAndLastName方法传进来的参数作为value这样就变成我们最终的{id=args[0],lastName=args[1]},也就是{id=4,lastName="Hello"}

假设我们有第三个参数,传入参数为[4,"Hello","ABC"],没有标注解,也没有在全局配置文件的setting中声明isUseActualParamName属性为true,那么就会出现以下现象

names集合为{0=id,1=lastName,2=2}

在param中则会变成{id=args[0],lastName=args[1],2=args[2]}, 也就是{id=4,lastName="Hello",2="ABC"}最后有一步

GENERIC_NAME_PREFIX就是"param",它会将所有变量都存放在刚刚那个param中,采用param1、param2….作为key进行命名。

这里有一个我现在还不明白的地方,刚刚在代码里很明显names={0=id,1=lastName,2=2},param为{id=args[0],lastName=args[1],2=args[2]},我们应该在<select>标签中却要这样书写

<select id="getEmpByIdAndLastName" resultType="com.figsprite.bean.Employee">
select id,last_name lastName,gender,email from tb_employee where id = #{id} and last_name = #{arg1}
</select>

貌似涉及到selectOne函数的实现,我日后再细看这部分代码

MyBatis源码分析1 参数映射分析的更多相关文章

  1. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  2. Mybatis源码阅读-配置文件及映射文件解析

    Mybatis源码分析: 1.配置文件解析: 1.1源码阅读入口: org.apache.ibatis.builder.xml.XMLConfigBuilder.parse(); 功能:解析全局配置文 ...

  3. Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)

    补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ...

  4. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  5. 精尽MyBatis源码分析 - MyBatis初始化(二)之加载Mapper接口与XML映射文件

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  6. MyBatis源码分析-MyBatis初始化流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

  7. MyBatis源码分析-SQL语句执行的完整流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

  8. 【MyBatis源码分析】select源码分析及小结

    示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是 ...

  9. Mybatis源码分析-StatementHandler

    承接前文Mybatis源码分析-BaseExecutor,本文则对通过StatementHandler接口完成数据库的CRUD操作作简单的分析 StatementHandler#接口列表 //获取St ...

随机推荐

  1. 设计模式のInterpreter Patern(解释器模式)----行为模式

    一.问题产生背景 有一句话“小明和小龙是好朋友”,我想分析其中谁是人,我想分析他们的关系等多种需求,那么我们应该如何处理,如果为每一个关系都进行判断?显然不合适,我们可以将二者的关系进行抽象处理,然后 ...

  2. go语言中如何模拟100个IP同时并发访问服务器,每个ip要重复访问1000次。每个Ip一分钟之内只能访问一次

    package main import ( "time" "sync" "fmt" "sync/atomic" ) ty ...

  3. 基于Django rest framework 和Vue实现简单的在线教育平台

      一.基于api前端显示课程详细信息 1.调整Course.vue模块 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 2 ...

  4. 鼠标右键打开命令行cmd(管理员身份)

    参考:https://blog.csdn.net/bdss58/article/details/54745380 添加到注册表 将下面命令保存为reg文件: Windows Registry Edit ...

  5. len()方法

    len() 方法返回对象(字符.列表.元组等)长度或项目个数 len()方法语法: len( 对象 )

  6. SQLAlchemy中的自引用

    SQLALCHEMY采用adjacency list pattern来表示类的自引用. 例如,对于类Node自引用: class Node(Base): __tablename__='node' id ...

  7. Java-Method类常用方法详解

    一.Method类的定义Method类位于 java.lang.reflect 包中,主要用于在程序运行状态中,动态地获取方法信息二.Method类的常用方法  1.getAnnotatedRetur ...

  8. 多模块调用Service失败

    最近在搭一个基础架构,整合项目. 在做多模块中调用的时候,在@Autowired的时候找不到service的bean. 解决方案: 需要在启动类加入扫描 @SpringBootApplication( ...

  9. JavaEE学习之Spring Security3.x——模拟数据库实现用户,权限,资源的管理

    一.引言 因项目需要最近研究了下Spring Security3.x,并模拟数据库实现用户,权限,资源的管理. 二.准备 1.了解一些Spring MVC相关知识: 2.了解一些AOP相关知识: 3. ...

  10. 【C# 复习总结】类、继承和接口

    1 类 定义新的数据类型以及这些新的数据类型进行相互操作的方法 定义方式: class Cat { } class Cat:object { } C#中所有的类都是默认由object类派生来的,显示指 ...