Mybatis Mapper接口方法参数处理

简介

这篇文档记录 mybatis 的DAO层接口参数以及方法返回值的处理,方便有需要的时候查找

正文

我们知道在 mybatis 定义一个接口参数可以加 @Param 注解,也可以不加,如果加了注解 sql 里取值就用注解的值,如果不加就默认用参数名称,比如下面的方法定义:

public interface UserMapper {
    @Select("select * from user where id = #{id}")
    User find(Long id);
    
	@Select("select * from user where id = #{name}")
    User find(@Param("name") Long id);
}

以上两种方法定义都可以生效,下面看下mysql里这块处理是怎么做的,因为mysql定义DAO只是定义一个接口,没有实现,mybatis会动态代理生成一个实现,关于这块内容不是本文重点,所以不做介绍,在执行sql方法的时候执行的是动态代理的方法,这里我贴出动态代理方法入口:

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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

mapper接口里方法的执行入口会是这里,调用invoke()方法进入cachedInvoker()方法,这个方法里会有一个创建MapperMethod类的地方,该类才是处理mapper接口参数的主要类,下面详细看下:

// 类名意味着该类对应 mapper 接口的一个方法
public class MapperMethod {
  // 这里是mapper接口一个方法最终要的两个属性,后面会详细分析
  private final SqlCommand command;
  private final MethodSignature method;

上面的类中,SqlCommand和MethodSignature 是最重要的两个属性,SqlCommand记录了一个方法的名称和方法类型:

public static class SqlCommand {
  // 方法名称
  private final String name;
  // 方法类型:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
  private final SqlCommandType type;

  /**
   * @param configuration mybatis全局唯一配置
   * @param mapper接口 Class,上面定义的UserMapper
   * @param method 接口方法,上面定义的find方法
   */
  public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    final String methodName = method.getName();
    final Class<?> declaringClass = method.getDeclaringClass();
    // 解析一个MappedStatement 
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
        configuration);
    if (ms == null) {
      if (method.getAnnotation(Flush.class) != null) {
        name = null;
        type = SqlCommandType.FLUSH;
      } else {
        throw new BindingException("Invalid bound statement (not found): "
            + mapperInterface.getName() + "." + methodName);
      }
    } else {
      // 初始化类属性值
      // xml文件中sql语句的ID
      name = ms.getId();
      // xml文件中sql语句的标签类型,INSERT等
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }
  }

  private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
      Class<?> declaringClass, Configuration configuration) {
    String statementId = mapperInterface.getName() + "." + methodName;
    if (configuration.hasStatement(statementId)) {
      // 这里是从mybtis启动加载后xml文件中的sql语句中根据id取出这条sql语句
      return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
      return null;
    }
    // 自定义的mapper文件到这里就执行结束了,下面的for循环是针对像 mbatis plus的 BaseMapper这种有继承关系做的
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
      if (declaringClass.isAssignableFrom(superInterface)) {
        MappedStatement ms = resolveMappedStatement(superInterface, methodName,
            declaringClass, configuration);
        if (ms != null) {
          return ms;
        }
      }
    }
    return null;
  }
}

MethodSignature 记录了方法的参数类型和返回类型:

  public static class MethodSignature {
    private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final boolean returnsOptional;
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    // 这个参数比较重要,是用来处理方法参数的
    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 解析方法定义的返回参数
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      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);
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      // 初始化 参数处理器
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }
}

下面看下ParamNameResolver 是如何处理方法参数的

public class ParamNameResolver {

  public static final String GENERIC_NAME_PREFIX = "param";
  // 方法参数是否使用实际名称,默认true
  private final boolean useActualParamName;
  // 存放参数下标和下标对应的name
  private final SortedMap<Integer, String> names;
  // 参数是否有@Param注解
  private boolean hasParamAnnotation;

  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;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // 跳过方法参数类型是RowBounds和ResultHandler
        continue;
      }
      // 这个参数代表sql中#{}实际要使用的值
      String name = null;
      // 这里是参数处理的核心逻辑,获取参数的所有注解
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        // 如果注解是@Param,name取注解value值,这里只判断@Param注解也就是说mybatis只关心这个注解,其他不关心
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      
      if (name == null) {
        // 到这里说明没找到@Param注解
        if (useActualParamName) {
          // 默认true,进入这里把name设置为参数本身的名称
          name = getActualParamName(method, paramIndex);
        }
        if (name == null) {
          // useActualParamName为false到达这里,使用参数下标作为name,但这里是最不常用的,因为会造成问题,具体不分析了
          name = String.valueOf(map.size());
        }
      }
      // 把当前这个位置sql里#{}要使用的参数名称放到map里
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

  // 这个方法是根据实际参数获取他要使用的名称
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      // 方法参数为空,直接返回
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      // 只有一个参数并且有@Param注解,获取names第一个值
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
      // 这里可能有多个参数或者没有@Param注解
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }
}

上面看完 mybatis mapper 接口的方法参数处理核心逻辑集中在ParamNameResolver的构造方法和getNamedParams()方法里。回到 MapperMethod 继续介绍,execute() 方法是 MapperMethod 中最核心的方法之一。execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作,其核心实现如下:

  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);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        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;
  }

对于 INSERT、UPDATE、DELETE 三类 SQL 语句的返回结果,都会通过 rowCountResult() 方法处理。方法的执行结果多数场景中代表了 SQL 语句影响的数据行数,rowCountResult() 方法会将这个 int 值转换成 Mapper 接口方法的返回值,具体规则如下:

  • 方法返回void,返回null
  • 方法返回Integer,直接返回int值
  • 方法返回Long,强转成Long
  • 方法返回Boolean,和 0 比较大小后返回

对于SELECT 语句查询到的结果规则如下:

  • 方法返回数组或列表,依赖sqlSession.selectList把结果包装成数组或列表
  • 方法返回map,依赖sqlSession.selectMap查询结果返回map
  • 方法返回cursor,依赖sqlSession.selectCursor查询结果返回cursor
  • 方法返回单个对象,依赖sqlSession.selectOne将查询结果返回,如果方法返回Optional,还要在包装一次

以上就是本文的全部内容。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值