# MyBatis(技术NeiMu):基础支持层(Transaction与Binding)

回顾

上一篇对MyBatis的数据源进行了分析,研究了MyBatis自身支持的两种数据源UnpooledDataSource与PooledDataSource

下面来看一下MyBatis对于事务是如何进行的

Transaction

在开发中,控制数据库的事务是一项非常重要的工作,而MyBatis则是使用了Transaction接口对数据库事务进行了抽象
在这里插入图片描述

在这里插入图片描述
可以看到Transaction事务接口支持5种方法

  1. commit:提交事务
  2. rollback:回滚事务
  3. close:关闭数据库连接
  4. getConnection:获取对应的数据库连接对象
  5. getTime:获取事务超时时间

而对于事务的管理,跟数据源一样采用的同样是抽象工厂模式来进行管理,先来看看事务的实现类型

在这里插入图片描述
可以看到,MyBatis自身有两种事务接口的实现类型

  1. ManagedTransaction
  2. JdbcTransaction

JdbcTransaction

在这里插入图片描述
JdbcTransaction有4个重要属性

  • connection:数据库连接
  • DataSource:数据源
  • TransactionIsolatinLevel:事务隔离等i
  • autoCommit:是否自动提交

在这里插入图片描述
对于其构造方法,一般会采用第一个构造方法,而第一个构造方法会初始化除了Connection之外的三种属性

获取连接

获取连接对应的是getConnection方法

public Connection getConnection() throws SQLException {
    //如果为null,调用openConnection进行打开连接
    if (connection == null) {
      openConnection();
    }
    return connection;
  }

可以看到,获取连接的主要操作委派到了openConnection方法中

源码如下

protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    //从数据源中获取
    connection = dataSource.getConnection();
    if (level != null) {
        //并设置隔离等级
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommit);
  }

可以看到,连接是从数据源DataSource进行获取的

提交、回滚、关闭

在这里插入图片描述
在这里插入图片描述
从源码上可以看到,对于提交、回顾、关闭,全部都是基于connection自身去做的,这里就不详细说明了

ManagedTransaction

在这里插入图片描述
可以看到,ManagedTransaction有4个成员属性

  1. dataSource:数据源
  2. level:事务隔离机制
  3. connection:数据库连接
  4. closeConnection:cloConnection配置

在这里插入图片描述
从构造方法上看,其同样也是除了connection属性外,其他都会进行初始化,因此我们也可以猜到其获取连接也是根据数据源DataSource来进行创建的
在这里插入图片描述
在这里插入图片描述
可以看到,其逻辑是一样的

并且ManagedTransaction是不支持事务提交、回滚的!!!!!! 其仅仅支持事务的关闭连接!!!!!!

在这里插入图片描述
仅仅支持事务连接关闭,并且事务的连接关闭会受closeConnect影响

在这里插入图片描述
这里也不再赘述了

可以看到两个事务之前的区别

  • JdbcTransaction支持事务提交、回滚,并且里面的操作都是Connection原生去实现的
  • ManagedTransaction不支持事务提交、回滚,不过支持修改closeConnect属性,也就是支持修改关闭Connection方法的逻辑

TransactionFactory

而对于工厂,对应的抽象工厂接口是TransactionFactory

在这里插入图片描述
在这里插入图片描述
并且对于TransactionFactory接口有2种抽象方法

  1. setProperties:设置一些工厂的配置(不需要一定去实现,默认实现为空)
  2. newTransaction:创建新的事务

在这里插入图片描述
而对应MyBatis支持的两种事务类型,MyBatis对于事务工厂同样也是有对应两个接口工厂实现类

  • JdbcTransactionFactory:创建JdbcTransaction
  • ManagedTransactionFactory:创建ManagerTransaction

JdbcTransactionFactory

JdbcTranSactionFactory用于创建JdbcTransaction

在这里插入图片描述
其源码也很简单, 直接new创建JdbcTransaction完事,但这里JdbcTransactinFactory并不支持对配置进行改动

ManagedTransactionFactory

ManagedTransactionFactory是创建ManagedTransaction的

在这里插入图片描述
其源码也并不复杂,对于创建事务也是直接new来创建的,但跟JdbcTransactionFactory不同的是,ManagedTransactionFactory重写实现了setProperties方法,但仅仅支持closeConnection这个配置

binding模块

绑定模块是MyBatis中的核心模块之一,其功能是让Mapper接口跟XML配置文件进行绑定!!!

绑定的好处是开发人员不需要执行对数据库操作的时候才发现错误,而是在初始化的时候,就能提供信息报错!!!!!!(假如没有绑定模块,使用接口执行数据库操作的时候才去找配置文件,那么也只有等待到执行数据库操作的时候才会报错)

还记得我们怎么使用MyBatis的吗?

  • 定义一个接口
  • 接口绑定resource对应目录下的配置文件,或者使用注解
  • 使用session.getMapper来获取对应接口的代理对象,用代理对象去拦截方法(这也是为什么MyBatis不用实例化接口也能使用接口的原因,关键就在于代理对象的产生)

我们先认识binding模块的核心接口的关系

在这里插入图片描述

  • MapperRegistry:Mapper接口以及MapperRegistryFactory的注册中心
  • MapperMethod:
  • MapperProxy:Mapper接口的代理对象

MapperRegistry

MapperRegistry是注册中心,负责Mapper接口(用Class对象来表示)以及MapperRegistryFactory的注册的

在这里插入图片描述
MapperRegistry的两个成员属性

  • Configuration:配置类,存储配置信息,后面再详细分析Configuration里面的内容,反正里面就是一大堆的配置信息
  • knownMappers:记录了Mapper接口与其对应MapperProxyFactory的关系,说白了就是Mapper接口与创建其代理对象的工厂的映射关系,这也是为什么说MapperRegistry是注册Mapper与其MapperProxyFactory的注册中心
注册Mapper与对应的MapperProxyFactory

对应的方法为addMapper,其方法的源码如下

public <T> void addMapper(Class<T> type) {
    //首先判断是不是接口
    //只能代理Mapper接口
    if (type.isInterface()) {
        //判断该Mapper接口是不是已经绑定了
        //这里只是使用knownMappers.containsKey方法来判断而已
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
        //loadCompleted代表下面是否加载完全
      boolean loadCompleted = false;
      try {
          //往底层的knownMappers中去添加Mapper接口与对应的MapperProxyFactory
          //相当于注册了Mapper接口
        knownMappers.put(type, new MapperProxyFactory<>(type));
          //使用MapperAnnotationBuilderParser对Mapper接口进行解析
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
          //进行解析
        parser.parse();
          //解析成功,才变为true
        loadCompleted = true;
      } finally {
          //可以看到这里并没有捕捉异常
          //当解析失败了之后
        if (!loadCompleted) {
            //解析失败,把注册了的Mapper接口移除掉
          knownMappers.remove(type);
        }
      }
    }
  }

从代码上可以看到,大概的步骤如下所示

  1. 先判断是不是Mapper接口,就是简单判断是不是接口类型,必须是接口类型才会继续进行下面的操作
  2. 判断当前的Mapper接口是不是已经注册绑定了
    1. 如果没有注册绑定,进行注册绑定
    2. 使用Mapper接口去创建MapperProxyFactory
    3. 添加进底层的knownMappers容器中
    4. 使用MapperAnnotationBuilder来进行解析
  3. 如果MapperAnnotationBuilder解析失败,将会移除之前的注册!!!!!!

这里关键的部分有几个,MapperProxyFactory是什么?MapperAnnotationBuilder用来干什么的?

这里先跳过,因为MapperAnnotationBuilder是涉及到XML解析和注解处理的

获取Mapper接口的代理对象

在需要执行某SQL时,会先调用MapperRegistry.getMapper的方法获取了Mapper接口的代理对象,下面就来看看方法的逻辑

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //从MapperRegistry注册中心的底层knownMapper容器里面去获取指定接口类型的MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    //判断是否为空
    if (mapperProxyFactory == null) {
        //如果没有对应的MapperProxyFactory,证明没有绑定,抛错
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    //拥有对应的MapperProxyFactory类型
    try {
        //使用对应的MapperProxyFactory去创建代理对象
        //并且是使用上进来的SqlSession
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  • 首先从底层得到knownMapper容器里面去获取指定接口类型的MapperProxyFactory
  • 判断MapperProxyFactory是否为空
    • 如果为空,证明没有绑定,抛错处理
    • 如果不为空,使用MapperProxyFactory去创建代理对象!

从这里就可以看到,MapperProxyFactory是负责去创建Mapper的代理对象的,下面就来看看这个MapperProxyFactory怎么回事

MapperProxyFactory

在这里插入图片描述
成员属性有两个

  • mapperInterface:该MapperProxyFactory为哪个接口类型创建代理对象
  • methodCache:方法缓存

同时也可以看到,唯一的构造方法仅仅只是初始化了指定Mapper接口而已

对于创建代理对象,从MapperRegistry也可以看到其调用的newInstance方法

在这里插入图片描述
可以看到,该方法是使用sqlSession、mapper接口,以及方法缓存去创建了一个MapperProxy对象,然后调用了重载方法(在红色框框的上面重载方法),使用JDK的动态代理去创建了一个被MapperProxy代理的Mapper接口对象并返回!!!

所以对于接下来我们使用Mapper进行的方法,实际上都会被MapperProxy给拦截!!

MapperProxy

MapperProxy其实就是Mapper接口的JDK动态代理的对象

在这里插入图片描述
可以看到其实现了InvocationHandler,就证明了其是一个代理对象,那么对于代理对象,关键点就在invoke方法上!不过在此之前还是说明一下一些关键成员属性的作用

  1. sqlSession:关联了的SqlSession,既连接会话
  2. mapperInterface:Mapper接口对应的Class对象
  3. mehodCache:用于缓存MapperMehodInvoker,其实本质上是缓存MethodHandler或者MapperMethod的,其中key是Mapper接口方法对应的Method对象,而Value则是对应的MethodHandler或者MapperMethod,两者会完成参数转换以及SQL语句的执行功能!但这里要注意的一点是两者并不会记录任何状态相关的信息,所以是可以在多个代理对象之间共享的!!!说白了其是一个方法缓存,用来缓存整个代理方法的逻辑,暴露给其他代理对象之间进行共享使用

那么MethodHandler和MapperMethod有什么区别呢?

其实在JDK1.8之前只有MapperMethod的,对应的就是接口中的方法;但JDK1.8之后,接口支持了默认方法,所以也就多出了MethodHandler,并且为了保持原来的架构,也就是只有一个方法缓存,所以必须要进行多一层封装,把两个类型都抽象成了MapperMethodInvoker!!!!!!

下面就可以看一下invoke方法的逻辑了,源码如下

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        //判断执行的是不是Object的方法
      if (Object.class.equals(method.getDeclaringClass())) {
          //如果是Object的方法就不进行拦截了
        return method.invoke(this, args);
      } else {
          //如果不是Object的方法,使用cachedInvoker获取缓存中的方法缓存然后进行invoke
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  • 对Object的方法不进行拦截
  • 对非Object的方法进行拦截

下面就来看看如何从缓存中取得方法缓存,源码如下,从中可以看到,缓存可以避免多次去重复创建对应的MethodInvoker(代理的细节其实位于MethodInvoker里面!!!),因为代理对象的细节是要变的(不同方法要执行的代理细节不一样,比如SQL不同),因此MyBatis又封装了一层MethodInvoker出来来统一抽象的代理细节,并且采用缓存技术来减少该代理细节的不断创建

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        //调用computeIfAbsent方法
        //该方法用来判断methodCache中是否有method对应的methodMapperInvoker
        //如果没有就会执行lambda的方法,此时m代表key,也就是现在传进去的method
        //lambda的方法是一个Funcation函数式接口,用来创建了一个新methodMapperInvoker并返回
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
          //判断方法是不是默认方法,也就是有没有默认的实现
        if (m.isDefault()) {
            //如果有默认的实现
          try {
              //
            if (privateLookupInMethod == null) {
                //返回DefaultMethodInvoker(JDK8)
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
                //返回DefaultMethodInvoker(JDK9)
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
            //如果不是默认方法,那就是一般的抽象方法
            //返回PlainMethodInvoker
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

可以看到,cachedInvoker方法其实就是从底层的方法缓存中取,如果没有就会去新建,并且根据接口方法的类型去进行创建,规则如下

  • 如果接口方法有默认实现,说白了就是有default修饰符,创建DefaultMethodInvoker,并且将其放入缓存中(DefaultMethodInvoker对应的就是MethodHandler)
  • 如果接口方法没有默认实现,只是一个普通的抽象方法,创建PlainMethodInvoker,并且将其放入缓存中(PlainMethodInvoker对应的就是MapperMethod)

下面就来看看DefaultMethodInvoker与PlainMethodInvoker

PlainMethodInvoker

在这里插入图片描述
可以看到MapperMethodInvoker是MapperProxy里面的一个内部接口,而PlainMethodInvoker是MapperProxy的一个内部类并且实现了MapperMethodInvoker接口,关键其内部组装了MapperMethod,并且重写的invoke方法调用了MapperMethod的execute方法

对于PlainMethodInvoker,其本质上就是组装了MapperMethod来,所以关键在于这个MapperMethod是什么!

MapperMethod

MapperMethod中封装了Mapper接口中对应方法的信息(一对一),以及对应SQL语句的信息,说白了其实真正的代理细节位于MapperMethod这里(对于接口的抽象方法),包括去解析注解上的SQL语句

在这里插入图片描述
成员属性有两个

  1. SqlCommand:记录了SQL语句的名称和类型
  2. MethodSignature:记录了Mapper接口中对应方法的信息

下面对着两个成员属性进行分析一下

SqlCommand

在这里插入图片描述
SqlCommand是MapperMethod里面的一个静态内部类,有两个成员属性

  • name:记录SQL语句的名称,形式为接口名称 + . + 方法名称
  • Type:记录SQL语句的执行类型

下面就看看其构造方法,源码如下

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    //获取方法名字
      final String methodName = method.getName();
    //获取被代理的方法所属的接口Class,其实不就是mapperInterface吗?
      final Class<?> declaringClass = method.getDeclaringClass();
    //使用MappedStatement去获取SQL的信息
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
    //如果获取不到,也就是没有绑定的SQL信息
      if (ms == null) {
          //判断是不是@Flush注解
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
           //将类型标记为Flush
          type = SqlCommandType.FLUSH;
        } else {
            //如果不是@Flush注解,抛错,表示没有绑定方法
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
          //如果获取到了绑定的SQL信息
          //获取SQL信息来进行自身初始化
          //比如SQL的id、SQL的类型
        name = ms.getId();
        type = ms.getSqlCommandType();
          //如果是Unknown类型,会进行抛错
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

说白了整个构造方法其实就做了一件事情,从配置信息中去获取MappedStatement,然后从里面去获取SQL参数并自身进行初始化,如果在这里获取不到MappedStatement,则会处理@Flush注解,该注解是用来刷新Statement的,特别是批量处理的时候(后面会分析),用于提前提交前面的SQL然后更新数据库信息的!!!!!

从这里可以看到,对于@Flush注解,MyBatis是不会记录在配置文件上的,而是在第一次执行该方法的时候,通过缓存的方法解析出来!

这里还有一个关键点就是MappedStatement的获取,对应的方法为resolveMappedStatement,源码如下

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
    //生成StatementId,格式为Mapped接口.方法名
      String statementId = mapperInterface.getName() + "." + methodName;
    //从配置文件中去寻找有没有对应的MappedStatement
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } 
    //如果Mapper接口与方法所在的接口相同,那就代表没有当前Mapper接口没有了,返回null
    //交由上层去处理,就很奇怪为什么上层有了Mapper接口还要去获取方法所在的Class对象
    //原来是考虑到接口的继承
    else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
    //如果此时配置上没有,那就可能出现在父类接口上了
    //获取当前Mapper接口的所有父类接口
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
          //遍历从父类上去找!
        if (declaringClass.isAssignableFrom(superInterface)) {
            //递归从父类上去寻找
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
  }

可以看到,MyBatis是支持Mapper接口进行继承的,并且对于方法对应的SQL信息如果在当前接口没有找到,就会尝试去父类去找,直到父类找完或者遇到了方法的所属接口。。。。。。

说白了,SqlCommond其实就是配置文件上的SQL信息,根据statementId去寻找对应的SQL(statementID格式是Mapper接口名.方法名)

MethodSignature

下面看另外一个属性,MethodSignature,其记录了绑定的方法的一系列信息,比如参数列表、返回值等

在这里插入图片描述
它也是MapperMethod里面的一个静态内部类,不过再认识它之前,先认识一下ParamNameResolver,顾名思义这个属性是用来处理Mapper接口中定义的方法的参数列表的,比如参数名称与参数位置索引之间的关系,还有@Param注解的解析,还有实参与参数名称的映射关系,其中一个ParamNameResolver就去处理一个方法的参数列表

在这里插入图片描述
在ParamNameResolver中有三个成员属性,如下

  1. hasParamAnnotation:是否有@Param注解
  2. names:用来存储参数的位置索引与参数名字的映射关系
  3. useActualParamName:是否使用真实的参数名字

比较重要的属性就是names了,是存储参数的位置索引与参数名字的映射关系,参数名称是通过@Param注解来指定的,如果没有指定的@Param注解,那么参数的名称也是参数索引,可以看到,其是一个SortMap,也就是可以进行排序的SortMap

而且,这里要注意,对于RowBounds类型或者ResultHandler类型的参数是不会被记录到name集合中的,所以对于如果参数列表中出现了这两个参数,可能会导致乱序的现象,这里提一下,RowBounds是用来分页的,而ResultHandler是用来处理结果集的

下面就看看ParamNameResolver是如何进行初始化的,初始化的操作都在构造方法上,源码如下

public ParamNameResolver(Configuration config, Method method) {
    //从配置上获取是否要使用真实参数名字,默认为True
    this.useActualParamName = config.isUseActualParamName();
    //获取方法的参数列表
    final Class<?>[] paramTypes = method.getParameterTypes();
    //获取方法上的参数列表上的注解
    //这里是一个二维数组,因为一个参数是可以使用多个注解的
    //比如paramAnnotations[0][0]代表第一个参数的第一个注解
    //paramAnnotations[0][1]代表第一个参数的第二个注解
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    //使用TreeMap来进行存储参数索引与参数名字的映射关系
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // 遍历参数
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        //判断当前参数是不是特定的类型,也就是前面所说的RowBounds和ResultHandler类型
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // 跳过了RowBounds与ResultHandler类型
        continue;
      }
      String name = null;
        //接下来就遍历当前参数里面的注解了
      for (Annotation annotation : paramAnnotations[paramIndex]) {
          //找到@Param注解
        if (annotation instanceof Param) {
            //将hasParamAnnotation标识为true
          hasParamAnnotation = true;
            //获取@Param注解里面的value属性!!!
          name = ((Param) annotation).value();
          break;
        }
      }
        //判断name此时是否为null
      if (name == null) {
        // 如果为null证明没有@Param注解,或者@Param没有使用上value属性
        //这时候都要MyBatis去生成另外的名字
          //判断是不是使用真实的参数名字
        if (useActualParamName) {
            //调用getActualParamName去寻找参数名字
            //这个是JDK1.8才能做到的
            //原理是找到在Class文件中定义的参数名字
            //但这里的参数名字一般都不会是我们写的,而是arg0、arg1的形式
            //只有给编译器开启了-parameters参数才会开启
          name = getActualParamName(method, paramIndex);
        }
          //如果找不到,一般来说就是关闭了useActualParamName选项
        if (name == null) {
          // 可以看到,名称直接使用容器的size属性,相当于是当前的索引值了
            //乱序的问题就是从这里发生,假如一个参数列表 [a,b,c]
            //b是RowBound类型的,因此会产生两个键值对
            //(0,0)与(2,1)【因为b跳过了,导致size不变,但索引值没跳】
            //不过一般我们采用getActualParamName就不会有这种问题
          name = String.valueOf(map.size());
        }
      }
        //最后就是已索引为Key,name为Value添加进names容器中
      map.put(paramIndex, name);
    }
    //最后初始化names属性
    names = Collections.unmodifiableSortedMap(map);
  }

步骤大概如下

  • 获取方法上的参数列表

  • 获取方法上的参数列表的注解列表

  • 创建TreeMap来进行储存

    • 遍历参数列表,判断是不是RowBound和ResultHandler类型,如果是则跳过不做处理,如果不是则遍历该参数的注解,找到@Param类型,如果有@Param注解则会找到其Value属性当作当前参数的name,如果此时还是没有找到name,判断有没有开启useActualParamName(使用真实参数名字【Class文件上的】),如果开启了,就使用JDK1.8自带的查找参数名称的功能,一般为arg0、arg1这种形式,如果没开启,就会使用TreeMap的Size来作为名字了(也是在这一步会产生乱序的问题)

    • 将参数与其名字形成键值对添加进TreeMap中

  • 初始化names属性

下面来看一下ParamNameResolver是如何获取name的,对应的方法为getNamedParams,该方法接收的参数是用户传入的实参列表,并且将实参与其对应名称进行关联,源码如下

public Object getNamedParams(Object[] args) {
    //获取names集合的size
    final int paramCount = names.size();
    //判空
    if (args == null || paramCount == 0) {
      return null;
    } 
    //判断是不是有开启@Param注解
    else if (!hasParamAnnotation && paramCount == 1) {
        //如果没有开启@Param注解,并且容器里面只有一个参数
        //直接取第一个
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
        //否则的话,即代表有多个参数或者开启了@Param注解
        //使用ParamMap来存储
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
        //遍历整个names集合,将其化成一个Set集合
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
          //添加进params中,此时使用名字作为key,实参列表对应的实参作为value
        param.put(entry.getValue(), args[entry.getKey()]);
        // 最后要判断是否要为参数创建默认的参数名称
          //默认的参数名称的格式为:"param+(第几个进来的参数,默认从1开始)"
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // 如果names集合中没有该默认的参数名称才添加
          //这是为了避免将某个参数的@Param的value给覆盖了!!!!!!
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

可以看到,这里是生成实参与形参名字的映射关系,在我们使用MySQL中给Mapper传入实参,实参还需要映射到SQL中,这一层的映射关系就是在这里完成的

步骤如下

  • 判断之前进来解析的参数列表是否只有一个,如果只有一个,直接返回传进来的实参
  • 如果不止一个或者使用了@Param注解,则是需要进行对应映射处理的
    • 创建一个ParamMap来进行存储
    • 遍历整个names集合,此时names集合里面的键值对是,key为索引,而value为参数名字,此时添加进ParamMap中,但此时得到key为参数的名字,而value为实参列表中对应参数索引的实参!!!此时就生成好了参数名称与实参的映射关系,然后还需要去添加一个默认的参数名称与实参的映射关系,默认的参数名称是param+序号(序号代表了第几个被添加进ParamMap中的,从1开始),但这个默认的参数名称不是一定要添加的,还需要判断有没有跟@Param指定的参数名称发生冲突,假如发生了冲突就不会去添加!!!!!!

现在讲完了ParamNameResolver了,回到我们的MapperSignature,回到构造方法上,源码如下

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
    //解析方法的返回值类型,前面已经在泛型分析上讲过了TypeParameterResolver了
    //因为返回值可能是一个泛型
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
    //如果返回值类型是一个Class类型
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
    //初始化returnsVoid、returnsMany、returnsCursor和returnsOptional
    //这几个属性都是判断类型而已
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);
      //mapKey是针对返回值属性为map并且使用了@MapKey注解的
    	//getMapKey仅仅只是解析了@MapKey注解里面的value属性而已
    //@MapKey的作用是可以将List转化为Map,并且指定元素中的属性来作为key,
    //而List中的元素成为对应的value
    //一般采用唯一键来作为Key
      this.mapKey = getMapKey(method);
    //判断是否返回map是根据有没有@MapKey注解的!!!
      this.returnsMap = this.mapKey != null;
    //初始化rowsBoundsIndex与resultHandlerIndex
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
    //初始化paramNameResolver
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

可以看到,构造方法都是初始化

  • 初始化返回值(使用TypeParameterResolver处理返回值,因为返回值是有泛型的)
  • 初始化returnsVoid、returnsMany、returnsCursor与returnsOptional属性,这4个属性都只是判断返回值类型而已,对应的是Void、Collection、Cursor、Optional类型而已
  • 判断是不是使用Map来作为返回值,依据是是否使用了@MapKey注解,并且如果使用了@MapKey注解会进行解析,把mapKey给初始化
  • 初始化rowBoundsIndex与resultHandlerIndex字段
  • 初始化paramNameResolver对象

下面说明一下rowBoundsIndex与resultHandlerIndex的初始化吧

回忆一下我们的paramNameResolver是对RowBounds与ResultHandler类型是跳开的,不会做这两种参数类型的映射关系,因此在上层就有必要去记录这两种类型的参数的位置

而且我们可以看到获取这两种类型的参数的位置使用的都是getUniqueParamIndex方法,源码如下

private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
      Integer index = null;
    //获取该方法的所有参数类型列表
      final Class<?>[] argTypes = method.getParameterTypes();
    //遍历去参数列表类型去进行查找
      for (int i = 0; i < argTypes.length; i++) {
          //如果找到了
        if (paramType.isAssignableFrom(argTypes[i])) {
            //判断之前是否出现过,没有出现就设为当前位置
            //出现过就抛错处理,因为不支持多个
          if (index == null) {
            index = i;
          } else {
            throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
          }
        }
      }
      return index;
    }

从方法上可以看到,内容仅仅只是遍历参数类型列表,但只允许出现一个,如果出现多个位置会报错,说白了就是出现了多个RowBounds类型、或者ResultHandler类型的参数

看完真个初始化过程后,下面就看最核心的execute方法吧,因为execute方法才是整个代理的核心,代理对象MapperProxy仅仅只是执行了execute方法,不过我们也可以大致猜到execute方法是干什么的,因为现在已经解析完了SQL,解析完了实参,剩下的就是调用SqlSession去注入实参执行相应的SQL语句了

源码如下

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
    //根据不同的SQL语句类型去调用SqlSession相应方法
  switch (command.getType()) {
    //insert方法
    case INSERT: {
       //实参映射,虽然执行的是convertArgsToSqlCommandParam
       //但其本质仅仅只是执行上面的getNamedParams方法
      Object param = method.convertArgsToSqlCommandParam(args);
        //sqlSession执行insert方法
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    //update方法
    case UPDATE: {
       //实参映射
      Object param = method.convertArgsToSqlCommandParam(args);
        //执行SqlSession的update方法
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    //delete方法
    case DELETE: {
       //实参映射
      Object param = method.convertArgsToSqlCommandParam(args);
        //执行sqlSession的delete方法
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    //select方法
    case SELECT:
       //处理返回值为void并且ResultSet要使用ResultHandler处理的方法
       //这种类型比较奇葩,明明是一个select,却返回值为void
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
          //因为返回值为void,所以result为null
        result = null;
      }
      //处理返回值为Collection的select
      else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      }
      //处理返回值为Map的的select
      else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      }
      //处理返回值为cursor的select
      else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      }
      //下面就是对于返回单个对象select的处理了
      else {
         //实参映射
        Object param = method.convertArgsToSqlCommandParam(args);
         //调用sqlSession的selectOne方法
        result = sqlSession.selectOne(command.getName(), param);
        //判断需要的返回值是不是Optional对象
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
           //使用结果集创建Optional对象
          result = Optional.ofNullable(result);
        }
      }
      break;
    //flush方法
    case FLUSH:
       //直接执行sqlSession的flushStatements方法
      result = sqlSession.flushStatements();
      break;
    //其他类型方法不支持,抛错
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  //如果结果集为null,并且返回值类型不是void类型会报错
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

可以看到,execute方法其实就是枚举了所有的执行情况进行对应的调用SqlSession的方法

  • Insert类型:实参映射,然后调用sqlSession的insert方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
  • Update类型:实参映射,然后调用SqlSession的update方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
  • Delete类型:实参映射,然后调用SqlSession的delete方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
  • Select类型:最复杂,需要根据返回值类型来去调用,Collectin类型、Void类型、Map类型、单个对象类型都是不同的调用方法

这里有个关键点,可以看到,对于返回的结果result,是使用Object类型来接收的

SqlSession方面的先不讲,等后面到了更上层的时候再进行分析SqlSession,下面来看一下对于insert、update、delete方法是如何处理MySQL返回结果的(MySQL对于DML语句的增删改返回的都是受到影响的行数),对应的方法为rowCountResult,源码如下

private Object rowCountResult(int rowCount) {
    final Object result;
    //如果返回值是void类型
    if (method.returnsVoid()) {
        //返回null
      result = null;
    }
    //如果返回类型是Integer
    else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      //不做处理,直接返回
      result = rowCount;
    }
    //如果返回值是Long类型
    else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      //强转从null
      result = (long) rowCount;
    }
    //如果是布尔类型
    else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      //返回处理结果是否大于0
      result = rowCount > 0;
    } else {
       //其他类型直接抛错
      throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

其实处理返回结果仅仅只是根据其返回值类型来做处理而已

  • 如果是void型,返回null
  • 如果是int型,直接返回处理的行数
  • 如果是long型,强转成Long后返回
  • 如果是boolean型,返回处理的行数是否大于0
  • 不支持其他类型处理,直接抛错

下面来看对于查询方法是如何做的

首先来看第一种情况,返回值为void型并且需要使用ResultHandler来进行处理,对应的方法为executeWithResultHandler(ResultHandler其实就是映射,说白了就是resultMap标签),源码如下

private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    //从配置文件上获取SQL语句对应的MappedStatement对象
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (!StatementType.CALLABLE.equals(ms.getStatementType())
        && void.class.equals(ms.getResultMaps().get(0).getType())) {
      throw new BindingException("method " + command.getName()
          + " needs either a @ResultMap annotation, a @ResultType annotation,"
          + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
    }
    //实参映射
    Object param = method.convertArgsToSqlCommandParam(args);
    //判断参数列表有没有RowsBounds类型的参数
    if (method.hasRowBounds()) {
       //如果有rowBounds,位置对应之前构造函数找到的rowBoundIndex!
        //从args实参中找到rowBounds
      RowBounds rowBounds = method.extractRowBounds(args);
        //执行select方法
      sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
        //执行select方法
      sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
  }

逻辑很简单,找到SQL信息、实参映射、判断需不需要进行分页、执行SqlSession的select方法

第二种情况,针对Collection集合类型的,对应的方法是executeForMany

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    //实参映射
    Object param = method.convertArgsToSqlCommandParam(args);
    //分页
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
        //调用selectList方法
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
    // 如果需要的返回值类型并不是返回Result的子类
    //说白了不是List接口
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
        //判断是不是数据类型,转换为数组类型
      if (method.getReturnType().isArray()) {
          //这里是采用遍历填充的方式来创建数组的
          //不过关键在于如何找对数组对应类型
        return convertToArray(result);
      } else {
          //不是数组类型,转换为其指定的类型,一般来说是一个Set集合
          //这里是使用到我们学习的MetaObject,将Result添加进MetaObject中
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

转换成集合也很简单,实参映射、分页、调用selectList方法,最后做结果集的处理,因为selectList方法返回的是一个List集合,对与其他类型的Collection要进行处理,比如Set或者数组

转换成数组的的代码如下

private <E> Object convertToArray(List<E> list) {
    //获取方法的返回值类型里面的元素类型
    //说白了就是获取数组中元素的类型
  Class<?> arrayComponentType = method.getReturnType().getComponentType();
    //使用该类型创建数组
  Object array = Array.newInstance(arrayComponentType, list.size());
    //判断是不是基本数组类型的数组
  if (arrayComponentType.isPrimitive()) {
      //如果是基本数据类型,需要进行遍历填充,不能直接转换!
    for (int i = 0; i < list.size(); i++) {
      Array.set(array, i, list.get(i));
    }
      //如果是基本数据类型直接返回
    return array;
  } else {
      //如果不是
      //使用list的toArray方法来转换
    return list.toArray((E[]) array);
  }
}

转换成其他Collection集合的代码如下

private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
    //从使用MetaObject来进行转换
    Object collection = config.getObjectFactory().create(method.getReturnType());
    MetaObject metaObject = config.newMetaObject(collection);
    metaObject.addAll(list);
    return collection;
  }

第三种情况,对于Map集合

private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
    Map<K, V> result;
    //实参映射
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
        //分页
      RowBounds rowBounds = method.extractRowBounds(args);
        //调用selectMap
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
    } else {
      result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
    }
    return result;
  }

实参映射、分页、调用selectMap方法

第四种情况,对于Cursor游标

private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
    Cursor<T> result;
    //实参映射
    Object param = method.convertArgsToSqlCommandParam(args);
    //分页
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
        //调用selectCursor方法
      result = sqlSession.selectCursor(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectCursor(command.getName(), param);
    }
    return result;
  }

可以看到,返回Currsor类型跟返回Map类型基本一致,实参映射之后、分页,最后交由SqlSession的selectCursor方法去完成处理

DefaultMethodInvoker

在这里插入图片描述
DefaultMethodInvoker也一样,是MapperProxy的一个内部类,并且实现了MapperMethodInvoker接口,关键其内部组装了MethodHandler,重写了invoke方法,调用了methodHandler的bind方法再执行invokeWithArguments方法

所以关键在于MethodHandler,MethodHandler是方法句柄,是JDK1.7提供的,用于支持动态方法的调用!所以并不是MyBatis自己封装的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值