【Mybatis】【SQL执行过程】【二】Mybatis源码解析-Mapper代理执行逻辑

1  前言

上节我们回顾了下 Mapper 接口的解析存放以及代理的入口和创建代理的过程,那么这节我们就来看下 MapperProxy 的代理执行逻辑。

2  源码分析

2.1  invoke 代理逻辑

Mapper 接口方法的代理逻辑首先会对拦截的方法进行一些过滤,以决定是否执行后续的操作。对应的代码如下:

/**
 * MapperProxy
 */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 如果方法是定义在 Object 类中的,则直接调用
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
      /**
       * 从缓存中获取 MapperMethodInvoker 对象,若缓存未命中,则创建 MapperMethodInvoker 对象
       * 然后执行
       */
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

那么我们继续看下 MapperMethodInvoker 的创建过程。

2.2  MapperMethodInvoker 的创建

首先会从缓存中取,注意一下这里的缓存是从 MapperProxyFactory里传过来的,也就是缓存是定义在 MapperProxyFactory 里的,key是方法,value是方法的执行器,我们看下创建的源码:

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    // 先从缓存中取,有的话直接返回
    MapperMethodInvoker invoker = methodCache.get(method);
    if (invoker != null) {
      return invoker;
    }
    return methodCache.computeIfAbsent(method, m -> {
      // 就是判断我们当前接口中的方法是不是 default的
      if (m.isDefault()) {
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return new DefaultMethodInvoker(getMethodHandleJava9(method));
          }
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException
            | NoSuchMethodException e) {
          throw new RuntimeException(e);
        }
      } else {
        /**
         * 首先会创建 MapperMethod对象
         * 再创建 PlainMethodInvoker 执行器
         */
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

抛开 defult 的方法,我们的接口方法都是会先创建 MapperMethod 方法,然后作为参数创建的 PlainMethodInvoker 执行器,那么接下里我们看看 MapperMethod 的创建过程。

3  MapperMethod 对象的创建

MapperMethod里边有两个属性 SqlCommand 、MethodSignature,创建 MapperMethod 对象的构造方法里也就是创建这两个对象的值,我们看下源码:

/**
 * MapperMethod
 * @param mapperInterface 我们的 Mapper 接口
 * @param method 当前执行的方法
 * @param config SqlSession 里的 Configuration 对象
 */
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  this.command = new SqlCommand(config, mapperInterface, method);
  this.method = new MethodSignature(config, mapperInterface, method);
}

那么我们就来看下创建 SqlCommand 的过程。

3.1  SqlCommand 的创建

我们来看下 SqlCommand的创建:

public static class SqlCommand {
  private final String name;
  private final SqlCommandType type;
  /**
   * @param configuration SqlSession 中的 configuration
   * @param mapperInterface 我们的 mapper 接口全类名
   * @param method 当前的方法
   */
  public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 获取到方法的名字
    final String methodName = method.getName();
    // 方法的来源类
    final Class<?> declaringClass = method.getDeclaringClass();
    // 解析 MappedStatement对象,我们在解析的时候讲过 我们的增删改查语句最后都会被封装进 MappedStatement 对象中
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
        configuration);
    if (ms == null) {
      // 看看方法上边有没有 Flush 注解
      if (method.getAnnotation(Flush.class) != null) {
        // 设置上名称和类型
        name = null;
        type = SqlCommandType.FLUSH;
      } else {
        // 找不到你的 MappedStatement 并且方法上边没有 Flush 注解就报找不到
        throw new BindingException("Invalid bound statement (not found): "
            + mapperInterface.getName() + "." + methodName);
      }
    } else {
      // 设置上名称和类型 
      name = ms.getId();
      type = ms.getSqlCommandType();
      // 不知道你语句是什么类型的情况下报错,也就是不知道你要干什么
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }
  }
}

可以看到 SqlCommand 就是检查你的 MappedStatement 对象是否存在以及是增删改查还是Flush,做一些基本的检查工作哈。

3.2  MethodSignature 的创建

MethodSignature 即方法签名,顾名思义,该类保存了一些和目标方法相关的信息。比如 目标方法的返回类型,目标方法的参数列表信息等。下面,我们来分析一下 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;
  /**
   * @param configuration SqlSession 中的 configuration
   * @param mapperInterface 我们的 mapper 接口全类名
   * @param method 当前的方法
   */
  public MethodSignature(Configuration configuration, Class<?> mapperInterface, Me
    // 解析方法的返回类型
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapp
    // 返回类型是实体类  比如 OrderPo、Map不带泛型的
    if (resolvedReturnType instanceof Class<?>) {
      this.returnType = (Class<?>) resolvedReturnType;
    // 返回类型是参数化类型 例如List、String这种不是参数化类型,凡是这种带有泛型的类型如Collection<String>、Map<String
    } else if (resolvedReturnType instanceof ParameterizedType) {
      this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRaw
    } else {
      this.returnType = method.getReturnType();
    }
    // 是不返回 void
    this.returnsVoid = void.class.equals(this.returnType);
    // 是不是返回多个
    this.returnsMany = configuration.getObjectFactory().isCollection(this.returnTy
    // 游标的
    this.returnsCursor = Cursor.class.equals(this.returnType);
    // 返回 Optional 的
    this.returnsOptional = Optional.class.equals(this.returnType);
    // 解析 @MapKey 注解,获取注解内容
    this.mapKey = getMapKey(method);
    this.returnsMap = this.mapKey != null;
    // RowBounds 分页参数 获取分页参数 RowBounds 在参数中的索引位置 发现有多个分页参数的话会报错
    this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
    // 获取 ResultHandler 在参数中的索引位置 也是一样有多个的话会报错
    this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
    // 解析我们的参数
    this.paramNameResolver = new ParamNameResolver(configuration, method);
  }
}

上面的代码用于检测目标方法的返回类型,以及解析目标方法参数列表。其中,检测返 回类型的目的是为避免查询方法返回错误的类型。比如我们要求接口方法返回一个对象,结 果却返回了对象集合,这会导致类型转换错误。关于返回值类型的解析过程先说到这,下面 分析参数列表的解析过程。

/**
 * @param config SqlSession 中的 Configuration
 * @param method 当前执行的方法
 */
public ParamNameResolver(Configuration config, Method method) {
  /**
   * 是否使用真实的名字
   * 默认是true 因为 config 中默认 useActualParamName = true
   */
  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++) {
    /**
     * 发现是 RowBounds 或者 ResultHandler 就跳过
     * RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
     */
    if (isSpecialParameter(paramTypes[paramIndex])) {
      continue;
    }
    String name = null;
    // 获取参数的注解
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      // 当发现参数有 @Param 注解的话,取注解中的值为名字
      if (annotation instanceof Param) {
        hasParamAnnotation = true;
        name = ((Param) annotation).value();
        break;
      }
    }
    /**
     * 如果没有@Param注解或者注解上的值是空的话,并且开启了 useActualParamName 会去取你参数的名字了
     * 这也是为什么我们有的时候不需要写@Param的原因
     */
    if (name == null) {
      if (useActualParamName) {
        /**
         * 比如 int getOne(String orderNo) 那么名字就是 orderNo
         * 此种方式要求 JDK 版本为 1.8+,且要求编译时加入 -parameters 参数,否则获取到的参数名仍然是 arg1, arg2, ..., argN
         */
        name = getActualParamName(method, paramIndex);
      }
      // 名字还是空的话 直接用0 1 2 3 ... 表示了
      if (name == null) {
        // use the parameter index as the name ("0", "1", ...)
        /**
         * 注意这里是 map.size() 不是 paramIndex
         * 因为如果参数列表中包含 RowBounds 或 ResultHandler,这两个参数会被忽略掉,这样将导致名称不连续。
         * 比如参数列表 (int p1, int p2, RowBounds rb, int p3)
         * - 期望得到名称列表为 ["0", "1", "2"]
         * - 实际得到名称列表为 ["0", "1", "3"]
         */
        name = String.valueOf(map.size());
      }
    }
    // 放进集合
    map.put(paramIndex, name);
  }
  names = Collections.unmodifiableSortedMap(map);
}

4  MapperMethod 执行前奏

我们的 MapperMethod 外边包了一层 PlainMethodInvoker,invoke 其实就是执行我们的 MapperMethod 的execute:

// PlainMethodInvoker invoke
cachedInvoker(method).invoke(proxy, method, args, sqlSession);
// PlainMethodInvoker
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  /**
   * 执行 mapperMethod 的 execute 
   * sqlSession 当前的会话对象
   * args       方法参数
   */ 
  return mapperMethod.execute(sqlSession, args);
}

我们来具体看下 MapperMethod 的execute:

/**
 * MapperMethod
 * @param mapperInterface 我们的 Mapper 接口
 * @param method 当前执行的方法
 * @param config SqlSession 里的 Configuration 对象
 */
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;
  // 根据语句类型 case
  switch (command.getType()) {
    case INSERT: {
      // 对传入的参数进行转换以及我们解析方法的参数名进行匹配
      Object param = method.convertArgsToSqlCommandParam(args);
      // 执行插入操作,rowCountResult 方法用于处理返回
      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()) {
        // 方法没返回值,但是有ResultHandler 说明通过ResultHandler 来获取查询结果
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // 执行查询操作,并返回多个结果
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        // 执行查询操作,并将结果封装在 Map 中返回
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        // 执行查询操作,并返回一个 Cursor 对象
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行查询操作,并返回一个结果
        result = sqlSession.selectOne(command.getName(), param);
        // 如果返回类型是Optional的,用Optional包装一下
        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;
}

整体的一个执行过程,就是根据 SqlCommand 的类型,进行不同的操作,我们这里先看下参数的匹配:

// MapperMethod 
public Object convertArgsToSqlCommandParam(Object[] args) {
  // 调用我们创建的MapperMethod时创建的参数解析器进行匹配 
  return paramNameResolver.getNamedParams(args);
}
/**
 * @param args 方法的实际传参
 * @return
 */
public Object getNamedParams(Object[] args) {
  // 参数的个数
  final int paramCount = names.size();
  // 如果传参为空或者参数长度为0就直接返回
  if (args == null || paramCount == 0) {
    return null;
  } else if (!hasParamAnnotation && paramCount == 1) {
    /**
     * 如果只有一个参数,并且不含@Param注解
     * 并且 value 是 Collection List 或者 Array 的话
     * 转换为ParamMap key就是我们平时forEach常见的 collection list array
     */
    Object value = args[names.firstKey()];
    return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
  } else {
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    for (Map.Entry<Integer, String> entry : names.entrySet()) {
      // 添加 <参数名, 参数值> 键值对到 param 中
      param.put(entry.getValue(), args[entry.getKey()]);
      // 比如 param1, param2,... paramN
      final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
      // 检测 names 中是否包含 genericParamName,不包含的话塞值
      // 包含的情况就是当我们显式将参数名称配置为 param1,即 @Param("param1")
      if (!names.containsValue(genericParamName)) {
        // 添加 <param*, value> 到 param 中
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    }
    return param;
  }
}

convertArgsToSqlCommandParam 该方法最终调用了 ParamNameResolver 的 getNamedParams 方法。

5  小结

这节我们大概看了下代理执行的前奏准备工作,下节我们针对不同的 SqlCommand进行逐一的分析,有理解不对的地方欢迎指正哈。 

posted @ 2023-03-06 20:44  酷酷-  阅读(78)  评论(0编辑  收藏  举报