美文网首页mybatis
mybatis 参数解析流程附相关案例

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

作者: 晴天哥_王志 | 来源:发表于2021-10-28 20:39 被阅读0次

    系列

    开篇

    • 本篇文章主要基于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对象。
    

    相关文章

      网友评论

        本文标题:mybatis 参数解析流程附相关案例

        本文链接:https://www.haomeiwen.com/subject/pnovaltx.html