MyBatis源码之:MapperMethod

原创
2017/08/05 14:28
阅读数 2.8K

MethodSignature

hasNamedParams

private boolean hasNamedParams(Method method) {
      boolean hasNamedParams = false;
      //方法上的每一个参数都可以有多个注解,所以是2维数组
      final Object[][] paramAnnos = method.getParameterAnnotations();
      for (Object[] paramAnno : paramAnnos) {
        //检查这些参数中的注解是否包含有Param注解
        for (Object aParamAnno : paramAnno) {
          if (aParamAnno instanceof Param) {
            hasNamedParams = true;
            break;
          }
        }
      }
      return hasNamedParams;
    }

hasNamedParams是判断方法上是否有Param注解的方法。如果你使用过Param注解就知道这个注解是处理参数别名的。

上面这个方法主要是为了判断需不需要处理参数的别名,只在getParam方法是需要知道是否需要处理别名。

下面看一下getParam方法:

getParam

private SortedMap<Integer, String> getParams(Method method, boolean hasNamedParameters) {
      final SortedMap<Integer, String> params = new TreeMap<Integer, String>();
      final Class<?>[] argTypes = method.getParameterTypes();
      for (int i = 0; i < argTypes.length; i++) {
        if (!RowBounds.class.isAssignableFrom(argTypes[i]) && !ResultHandler.class.isAssignableFrom(argTypes[i])) {
          String paramName = String.valueOf(params.size());
          if (hasNamedParameters) {
            paramName = getParamNameFromAnnotation(method, i, paramName);
          }
          params.put(i, paramName);
        }
      }
      return params;
    }

getParam方法主要是把方法的参数处理为对应位置和名称的集合。如果是RowBounds和ResultHandler就不处理。如果有Param别名就处理为Param别名的值,如果没有Param别名就处理为对应位置的索引值(参数位置-1)。

convertArgsToSqlCommandParam

public Object convertArgsToSqlCommandParam(Object[] args) {
      final int paramCount = params.size();
      if (args == null || paramCount == 0) {
        return null;
      } else if (!hasNamedParameters && paramCount == 1) {
        return args[params.keySet().iterator().next()];
      } else {
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : params.entrySet()) {
          param.put(entry.getValue(), args[entry.getKey()]);
          // issue #71, add param names as param1, param2...but ensure backward compatibility
          final String genericParamName = "param" + String.valueOf(i + 1);
          if (!param.containsKey(genericParamName)) {
            param.put(genericParamName, args[entry.getKey()]);
          }
          i++;
        }
        return param;
      }
    }

convertArgsToSqlCommandParam方法主要是把方法参数转换为SQL命令参数。前面我们知道params存放的是方法参数位置与参数位置或参数位置与别名的SortedMap,convertArgsToSqlCommandParam把处理成了参数位置与参数位置或者别名与参数位置的ParamMap(HashMap),并且把参数都加上了一个paramX的别名,其中X是参数的位置索引。

现在知道经常出现的错误param1,param2,param3......出现在什么地方了吧。

getMapKey

 private String getMapKey(Method method) {
      String mapKey = null;
      if (Map.class.isAssignableFrom(method.getReturnType())) {
        final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
        if (mapKeyAnnotation != null) {
          mapKey = mapKeyAnnotation.value();
        }
      }
      return mapKey;
    }

这个方法是处理方法上的MapKey注解的,这个是在select查询返回值是Map类型使用的。 这个还简单就是检查方法的返回值是不是Map类型,如果是就检查方法上是不是有MapKey注解,有就获取,没有就返回null。

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;
    }

getUniqueParamIndex方法就是检查方法是否有唯一参数类型,并且返回指定类型的参数的位置索引。就是方法指定类型paramType的参数最多只能有一个。其实只是为了处理2种类型的方法参数一种是RowBounds,另一种是ResultHandler。

SqlCommand

对于SqlCommand,我们看一下构造方法就可以了:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException {
      String statementName = mapperInterface.getName() + "." + method.getName();
      MappedStatement ms = null;
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      if (ms == null) {
        throw new BindingException("Invalid bound statement (not found): " + statementName);
      }
      name = ms.getId();
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }

SqlCommand其实只想知道2件事情,1是MappedStatement的id,而是MappedStatement的SQL类型是insert,update,delete,select的哪一种。

mapperInterface其实就是mapper接口,所以我们还清楚的知道mapper接口和xml的映射关系。就是mapper接口的权限定名称+方法的名称作为一个语句(MappedStatement)的key。

看到上面有一个issue35,这个是处理接口继承的问题,就是为了解决继承的方法也可以映射的到mapperInterface对应的xml中。可以参考gitHub的#issue35

MapperMethod

execute

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      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;
  }

我们可以看到execute调用的是SqlSession执行了SQL语句,execute方法当然非常重要,但是这里我们先不关心SqlSession对于语句的执行逻辑。我们关心的是执行之前对Mapper接口的处理。

rowCountResult方法就是对insert,update,delete语句影响行数的转换没有什么说的,感兴趣可以自己看一下代码。注意这3个的支持int,Integer,long,Long,boolean,Boolean这3中类型。所以不要说我知道只会删除一个我的返回值就使用short类型。

接下来我们看一下具体查询之前的处理的3个方法:

executeWithResultHandler

private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
    MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
    if (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);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
    } else {
      sqlSession.select(command.getName(), param, method.extractResultHandler(args));
    }
  }

convertArgsToSqlCommandParam前面已经说了把接口方法的参数转换为SQL命令使用的参数。

先判断方法的参数中是否包含RowBounds类型,这个是在MethodSignature#getUniqueParamIndex检查唯一类型参数就处理了。如果有就把类型参数提取出来,提取方法很简单就不说了。最后把RowBounds参数注入到查询语句中。

提取ResultHandler和RowBounds一样,逻辑和代码都是一样的,只是类型参数不同而已。

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);
      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;
  }

executeForMany主要是处理有多条结果的情况。方法的逻辑非常清晰,如果Mapper接口的方法的返回值类型不是结果的List类型,那么就特殊处理一下。如果是方法的返回类型是数组就将List转换为数组。否则就转换为返回值类型的集合类型。

我们来看一下将List转换为数组的方法,和指定类型的集合的方法:

List转换为数组:

convertToArray

@SuppressWarnings("unchecked")
  private <E> E[] convertToArray(List<E> list) {
    E[] array = (E[]) Array.newInstance(method.getReturnType().getComponentType(), list.size());
    array = list.toArray(array);
    return array;
  }

这是一个非常典型的集合转数组的方法。因为不知道具体的类型,所以使用Array类来动态生成数组。因为已经检查了方法的返回值是数组类型所以可以直接使用getComponentType方法。是什么让他有自信可以使用"unchecked",是因为Mapper接口的返回值类型是和MappedStatement的ResultType(ResultMap)是对应的,就是E的实际类型,如果不匹配就出错。

convertToDeclaredCollection

private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
    Object collection = config.getObjectFactory().create(method.getReturnType());
    MetaObject metaObject = config.newMetaObject(collection);
    metaObject.addAll(list);
    return collection;
  }

convertToDeclaredCollection就是把结果转换为Mapper接口返回值对应的类型的集合类型。如果感兴趣可以看一下DefaultObjectFactory的代码,代码还是比较简单的,就是把指定的类型转化为特定的类型,例如返回是Map,那么就构造一个HashMap。为什么不用new,那是因为没有办法处理泛型,另外最重要的没有办法处理所有的类型。

executeForMap

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);
      result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds);
    } else {
      result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey());
    }
    return result;
  }

executeForMap方法就是把结果处理成Map。这个可以看一下SqlSession的实现。

我们看到3个查询都处理了RowBounds参数,所以现在知道在Mapper接口中传入pageNo,pageSize,然后还用动态SQL来处理limit语句有多傻了吧。

总结

  1. 处理了RowBounds参数
  2. 处理了ResultHandler参数
  3. 处理了@Param注解
  4. 处理了@MapKey注解
  5. 处理了返回结果
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部