写在前面

相信Java开发的小伙伴对MyBatis一定不陌生,它是一款优秀的持久层框架,支持自定义 SQL、存储过程以及高级映射。同时免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,并且可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO为数据库中的记录。今天我们要讨论的,就是MyBatis中Mapper接口参数是如何包装并且与XML映射文件进行匹配映射的。

一、ParamNameResolver:参数解析器

首先看一段代码 上述代码中,Mapper接口在不同的方法中定义了不同类型的入参。MyBatis在处理参数映射时,参数映射的类型都会经过ParamNameResolver的统一处理并且封装成对应的类型。下面我们看一下具体的实现:

1.1 构造ParamNameResolver对象

MyBatis在处理执行流程时,会使用动态代理创建出目标对象对应的代理对象,入口在 org.apache.ibatis.binding.MapperProxy中 在方法中会判断被代理的目标对象是否为Object类型,如果为非Object类型,则创建对应的调用器执行调用方法。

这里会根据判断创建一个PlainMethodInvoker类型的调用器,同时会创建MapperMethod,MapperMethod是一个用来记录目标方法全名称和类型的对象,在其内部封装了MethodSignature方法签名,在创建MethodSignature时会调用ParamNameResolver的构造方法创建ParamNameResolver对象。我们来看一下ParamNameResolver构造方法的具体实现

 public ParamNameResolver(Configuration config, Method method) {
    this.useActualParamName = config.isUseActualParamName();
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 获取方法参数中的注解集合
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    // 获取注解集合的长度
    int paramCount = paramAnnotations.length;
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
      // 循环注解集合,如果有@Param类型的注解,将注解的value赋值给name变量
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      // 如果name == null,则证明没有@Param类型的注解
      if (name == null) {
      // 如果允许使用方法签名中的名称作为语句参数名称,则将参数名称赋值给name
        if (useActualParamName) {
          name = getActualParamName(method, paramIndex);
        }
        // 如果useActualParamName为false,则使用数组下标作为name使用
        if (name == null) {
          name = String.valueOf(map.size());
        }
      }
      // 将参数下标和name值映射起来
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

上述构造方法内部将参数列表和参数所在的数组下标进行映射并封装到数据结构为SortMap的属性names中。即如果useActualParamName设置为true且未使用@Param,name中存放的值为{0=arg0, 1=arg1....},如果useActualParamName设置为false且未使用@Param,name中存放的值为{0=0, 1=1....},如果使用@Param注解,name中存放的值的value为@Param中的value。 将参数封装到SortMap的names属性中之后,MapperMethod会调用execute()方法执,在execute方法中,会判断当前方法是增删改查中的哪种类型,同时再次调用ParamNameResolver的getNameParams()方法处理参数。 下面是ParamNameResolver的getNameParams()的具体实现:

 public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // 判断参数个数,如果数量为0,则没有入参
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
    // 如果只有一个入参,则从入参列表中根据下标直接出去参数
      Object value = args[names.firstKey()];
      // 如果集合类型,则进行集合类型的包装
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
    // 如果有多个入参,则将参数封装到Map中,
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 将names中的value作为key,key作为列表的下标进行取出后作为value放到map中
        param.put(entry.getValue(), args[entry.getKey()]);
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

这里要注意两点:

  • 如果单个参数会判断一下是否为集合类型,如果是集合或数组类型,ParamNameResolver也会将其包装成Map,其中key为集合的类型,value为具体的入参。下面是具体的代码: 如果入参对象实现了Collection接口,则key为“collection”。如果进一步判断入参对象实现了List接口,则key变更为“list”,如果是数组类型,则key为“array”。

  • 如果是多个入参,在ParamMap类型的param中除了会放置@Param注解标识的参数之外,还会根据参数的个数,以param1~paramN作为key,对应参数下标的value放入到param中。这样做的主要原因是确保不要覆盖以@Param命名的参数。

上述就是Mapper接口中针对不同参数类型、不同参数列表的长度来说,MyBatis对应的不同的处理流程。下面是整个参数封装的流程图:

二、PrepareStatement:预编译对象

将参数构建好后,接下来就是将参数对象向下传递,在Mapper接口对应的XML文件中,可以通过#{}的方式从参数列表取出对应的参数值,然后通过构建的PrepareStatement对象对数据库进行增删改查操作,那么MyBatis是如何将参数对象和#{}中的值对应起来的呢?我们一起来看一下。 构建好的参数会通过SqlSession传递给执行器,执行器是MyBatis的四大对象之一,负责调度PrepareStatement处理器,结果集映射处理器和类型处理器。默认使用SimpleExecutor。在SimpleExecutor类中,我们可以找到参数对象和#{}中的值对应起来的逻辑,下面是具体代码: 这里要说明一下PrepareStatement,PrepareStatement是Statement的子接口,表示预编译SQL语句对象,通过占位符(?)来拼接SQL。PrepareStatement和Statement都可以表示语句对象,PrepareStatement的优势在于:

  • 拼接SQL上,操作更简单
  • 性能更加高效,但是需要取决于数据库服务器是否支持
  • MySQL:不支持
  • Orcale:支持
  • 安全性更高,可防止SQL的注入攻击 下面是PrepareStatement的执行流程图 MyBatis默认使用PreparementStatement,可以选择STATEMENT和CALLABLE。在XML映射文件中可以指定该属性。 下面是官方文档中关于PreparementStatement的介绍 图片

三、设置PrepareStatement的参数

这里的核心逻辑主要在DefaultParameterHandler的setParameters方法中

 @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 将XML映射文件中#{}标识的参数获取到
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
    // 循环遍历
      for (int i = 0; i < parameterMappings.size(); i++) {
      // 取出其中的一个ParameterMapping对象
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            // 如果是单个参数,直接赋值给value
            value = parameterObject;
          } else {
          // 如果是多个参数,转换成MetaObject对象
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
          // 最后通过TypeHandler找到对应的类型处理器设置参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

这里要说明几点:

  1. 如果参数类型是常用类型(类似String、Integer、Long等),可以通过TypeHandlerRegistry根据jdbcType直接找到对应类型的TypeHandler

2.如果参数为Map类型、Java Bean,则会通过MetaObject的解析,在MetaObject的getValue()方法中,会将XML映射文件中的参数逐个包装成PropertyTokenizer对象,PropertyTokenizer对象,如果映射文件中的参数非级联属性,则可以直接获取,如果是级联属性,或根据#{}中的“.”符号组装成一个树形结构的对象用于解析

PropertyTokenizer对象的构造方法

从MetaObject中获取Mapper接口对应的参数值

最终,会从ObjectWapper中根据XML映射文件中的key去Mapper接口的参数列表中匹配,这里又有两种情况,如果是Mapper接口的参数最终封装为Map类型,则直接通过MapWapper的get()方法获取,如果是Java Bean,则通过BeanWapper反射调用来获取入参对象的属性值。

Map类型

Java Bean类型

反射调用

3.找到了对应的TypeHandler后,会通过对应TypeHandler的setNonNullParameter方法对PrepareStatement对象进行参数的设置,例如String类型的TypeHandler 下面是TypeHandler的继承关系图 图片

总结

以上就是MyBatis中处理Mapper接口入参并与XML映射文件中#{}内的参数进行匹配的整个过程。通过上面的分析,我们可以得出几个结论。

  • 如果只有一个参数且非Java Bean类型或Map类型的对象时,在XML映射文件中的#{}内可以任意写变量,不需要和Mapper接口中的参数名称匹配。因为当只有一个参数时,可以直接找到对应的TypeHandler,不需要进行匹配。

  • 如果只有一个参数且是Map类型(多个入参对象最后也会封装成Map)或者Java Bean类型,可以在#{}直接使用Map的key或者Java Bean的属性名称进行取值。因为#{}中的参数值可以直接作为key去Map中获取对应key的value或者通过反射的方式获取Java Bean中对应属性的值

  • 当有多个参数且未使用@Param修饰时,如果useActualParamName属性为true时,在XML映射文件的#{}中,只能根据参数在参数列表中的位置使用arg0~argN或者param1~paramN作为key进行取值。如果useActualParamName属性为false时,则使用0~N或者param1~paramN作为key进行取值。

    以上就是MyBatis中Mapper接口参数的包装流程及映射规则,可能在设置PrepareStatement的入参时,根据#{}中的参数匹配Mapper接口中的参数值这里逻辑相对来说比较繁琐,所以还是建议感兴趣的小伙伴可以用这篇文章作为辅助,自己亲自动手搭建MyBatis的调试环境并阅读以下源代码,方便自己更好的理解~