mybatis 参数解析流程附相关案例

系列

开篇

  • 本篇文章主要基于mybatis-3.3.0版本进行源码分析,目标在于记录mybatis在参数解析过程。
  • 了解参数解析过程是为了更好的写mybaits的xml预发以及更好的适应mybatis的参数拦截插件。
  • 文章会讲解清楚两个问题点,包括mybatis的执行过程中参数的传递形式和对List的特殊处理。


参数解析过程

  • mybatis的参数解析过程主要包含三个步骤,包括:解析方法参数的别名,构建方法参数对象,特殊处理集合对象
  • 解析方法参数的别名:负责解析方法的参数和参数注解,构建参数下标及对应的别名的映射关系。
  • 构建方法参数对象:结合参数别名映射关系,构建参数别名和参数对象的映射关系。
  • 特殊处理集合对象:针对参数对象为单一集合的场景人工生成list/collection的映射关系。


解析方法参数的别名

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  // mapperInterface会对应的mapper的 interface 接口
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public static class MethodSignature {

    // 省略其他不强相关的代码
    private final SortedMap<Integer, String> params;
    private final boolean hasNamedParameters;

    // 解析方法签名
    public MethodSignature(Configuration configuration, Method method) {
      // 判断是否有@Param的注解
      this.hasNamedParameters = hasNamedParams(method);
      // 通过getParams来获取对应的参数值
      this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters));
    }

    // 解析方法的参数
    private SortedMap<Integer, String> getParams(Method method, boolean hasNamedParameters) {
      final SortedMap<Integer, String> params = new TreeMap<Integer, String>();

      // 1、获取方法的参数类型
      final Class<?>[] argTypes = method.getParameterTypes();

      // 2、遍历方法的参数类型构建参数下标和别名的映射关系
      for (int i = 0; i < argTypes.length; i++) {
        if (!RowBounds.class.isAssignableFrom(argTypes[i]) && !ResultHandler.class.isAssignableFrom(argTypes[i])) {
          // 默认以参数的下标值作为value值
          String paramName = String.valueOf(params.size());
          // 如果有注解就遍历该参数对应的注解名字
          if (hasNamedParameters) {
            paramName = getParamNameFromAnnotation(method, i, paramName);
          }
          params.put(i, paramName);
        }
      }

      // 返回的params以参数下标作为key,以下标或者别名作为value
      return params;
    }

    // 从@Param注解中获取对应的别名
    private String getParamNameFromAnnotation(Method method, int i, String paramName) {
      // 遍历方法的所有参数注解,getParameterAnnotations返回的是一个二维数组
      // 第一维是参数的下标,第二维是改参数对应的多个注解
      final Object[] paramAnnos = method.getParameterAnnotations()[i];

      // 如果存在@Param注解就获取注解对应的别名
      for (Object paramAnno : paramAnnos) {
        if (paramAnno instanceof Param) {
          paramName = ((Param) paramAnno).value();
          break;
        }
      }
      return paramName;
    }
}
  • 通过method.getParameterTypes()获取方法的所有参数对象。
  • 遍历方法的参数,解析该参数对应的注解@Param对应的别名,如果没有注解用参数下标作为别名。
  • 构建方法参数的别名Map对象,其中key为参数顺序,value为别名或参数的下标顺序。


构建方法参数对象

public class MapperMethod {

  public static class MethodSignature {

    // 解析入参返回mybatis内部处理的参数
    public Object convertArgsToSqlCommandParam(Object[] args) {
      final int paramCount = params.size();

      // 1、参数为空的场景直接返回
      if (args == null || paramCount == 0) {
        return null;

      // 2、没有@Param注解修饰且只有一个参数的场景
      } else if (!hasNamedParameters && paramCount == 1) {
        return args[params.keySet().iterator().next().intValue()];

      // 3、其他的场景,包含@Param注解或参数个数大于1
      } else {
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        // 遍历所有的方法参数,以别名或者下标作为key,value未
        for (Map.Entry<Integer, String> entry : params.entrySet()) {
          // entry.getValue()返回的是@Param对应的别名 或 参数的顺序
          param.put(entry.getValue(), args[entry.getKey().intValue()]);

          // 额外添加param0、param1作为key,value为对应的参数值
          final String genericParamName = "param" + String.valueOf(i + 1);
          if (!param.containsKey(genericParamName)) {
            param.put(genericParamName, args[entry.getKey()]);
          }
          i++;
        }
        return param;
      }
    }
}
  • 针对不同场景返回不同的参数映射map,1、如果参数为空的场景直接返回空对象;2、如果没有@Param注解修饰且只有一个参数的场景直接返回参数对象;3、其他场景构建参数的Map对象进行返回。
  • 所以在mybatis的执行过程中的参数存在三种形态,分别是空对象、Bean/java数据原生对象,Map对象。
  • 针对其他场景构建的参数,依据解析方法参数的别名过程的结果构建参数Map对象,我们以别名或参数下标作为key,以参数对象作为value的Map对象。因为向后兼容的需要Map中会保存字符串param1(参数下标位置)作为key,参数对象作为Value。


解析方法参数的过程

public class MapperProxy<T> implements InvocationHandler, Serializable {

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }

    // 针对被调用的方法 method 创建对应的 MapperMethod对象。
    final MapperMethod mapperMethod = cachedMapperMethod(method);

    // 通过mapperMethod执行 execute 动作
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
}
  • MapperProxy的invoke的过程会针对调用的方法创建MapperMethod对象。
  • MethodSignature的创建会构建被调用的方法的签名信息,构建了参数的别名信息。
public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        // 执行前会进行参数转换
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        // 执行前会进行参数转换
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        // 执行前会进行参数转换
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    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;
  }


  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    // 执行前会进行参数转换
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.<E>selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

  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);
      result = sqlSession.<T>selectCursor(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.<T>selectCursor(command.getName(), param);
    }
    return result;
  }
}
  • MapperMethod执行SQL 操作的时候会通过convertArgsToSqlCommandParam解析参数,后续以解析后的参数执行 SQL 语句。


特殊处理集合对象

public class DefaultSqlSession implements SqlSession {

  @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      return map;
    } else if (object != null && object.getClass().isArray()) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    return object;
  }
}
  • DefaultSqlSession#wrapCollection的过程在MethodSignature#convertArgsToSqlCommandParam之后,针对只有一个参数对象且没有@Param标注别名的列表对象会构建一个Map进行返回。
  • 回忆下mybatis的xml定义中经常会遍历列表对象,存在<foreach collection="list">的语句,这里的list就是在这个过程中生成的。


参数案例

场景一:
List<Object> batchSelectByXxx(@Param("xxx") List<String> xxxList)
参数映射:构建以xxx为key,xxxList为value的Map对象。

场景二:
int batchInsert(List<Object> xxxList)
参数映射:构建以list/collection为key,xxxList为value的Map对象

场景三:
int update(Object record)
参数映射:构建以Object为参数对象,没有Map。

场景四:
List<Object> listByParam(@Param("paramA") String strA, @Param("paramB") String strB)
参数映射:构建以paramA为key,strA为value;以paramB为key,strB为value的Map对象。

场景五:
List<Object> listByParam(String strA, String strB)
参数映射,构建以0为key,strA为value;以1为key,strB未value的Map对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容