MyBatis 基础支持层:Binding 模块

Java 接口与 mapper.xml 之间的映射

在 XML 代理开发中,我们会为每个 mapper.xml 配置文件创建一个对应的 Mapper 接口,无须提供 Mapper 接口实现,就可以直接调用 Mapper 接口对象的方法执行 mapper.xml 配置文件中的 SQL 语句。对此,我们可以引出几个疑问:

  • 为什么需要 Mapper 接口来执行对应的 SQL 语句?
  • 为什么无须提供 Mapper 接口的实现类?
  • 实际使用的 Mapper 接口对象是什么?
  • Mapper 对象是如何创建的?

接下来让我们通过分析源码来解决这些疑问。

在 MyBatis 中,实现 Mapper 接口与 Mapper.xml 配置文件映射功能的是 Binding 模块org.apache.ibatis.binding 包下),其中涉及的核心类如下图所示:

Binding 模块

1 MapperRegistry

MapperRegistry 是 MyBatis 初始化过程中构造的一个对象,主要作用就是统一维护 Mapper 接口以及这些 Mapper 的代理对象工厂。我们先来看 MapperRegistry 中的核心字段:

1
2
Configuration config;
Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  • config:指向 MyBatis 全局唯一的 Configuration 对象,其中维护了解析之后的全部 MyBatis 配置信息。
  • knownMappers:维护了所有解析到的 Mapper 接口以及 MapperProxyFactory 工厂对象之间的映射关系。

在 MyBatis 初始化时,会读取全部 Mapper.xml 配置文件,还会扫描全部 Mapper 接口中的注解信息,之后会调用 MapperRegistry.addMapper() 方法填充 knownMappers 集合。在 addMapper() 方法填充 knownMappers 集合之前,MapperRegistry 会先保证传入的 type 参数是一个接口且 knownMappers 集合没有加载过 type 类型,然后才会创建相应的 MapperProxyFactory 工厂并记录到 knownMappers 集合中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) { // 确保是接口
    if (hasMapper(type)) { // 确保没被加载过
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 先添加映射到 knownMappers
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // 接口方法、注解解析
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

在我们使用 XxxMapper.find() 方法执行数据库查询的时候,MyBatis 会先从 MapperRegistry 中获取 Mapper 接口的代理对象,这里就使用到 MapperRegistry.getMapper() 方法,它会拿到前面创建的 MapperProxyFactory 工厂对象,并调用其 newInstance() 方法创建 Mapper 接口的代理对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 创建 Mapper 接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

2 MapperProxyFactory

MapperProxyFactory 的核心功能就是创建 Mapper 接口的代理对象,其核心原理就是 JDK 动态代理。

MapperRegistry 中会依赖 MapperProxyFactorynewInstance() 方法创建代理对象,如下代码所示,其内部使用的 InvocationHandler 实现是 MapperProxy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  // 调用下方重载方法
  return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
  // 创建实现了 mapperInterface 接口的动态代理对象,这里使用的 InvocationHandler 实现是 MapperProxy
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
    new Class[] { mapperInterface }, mapperProxy);
}

3 MapperProxy

通过分析 MapperProxyFactory 这个工厂类,我们可以清晰地看到 MapperProxy 是生成 Mapper 接口代理对象的关键,它实现了 InvocationHandler 接口。下面我们先来介绍一下 MapperProxy 中的核心字段:

1
2
3
4
5
SqlSession sqlSession;
Map<Method, MapperMethodInvoker> methodCache;
Class<T> mapperInterface;
Constructor<Lookup> lookupConstructor;
Method privateLookupInMethod;
  • sqlSession:记录了当前 MapperProxy 关联的 SqlSession 对象。在与当前 MapperProxy 关联的代理对象中,会用该 SqlSession 访问数据库。
  • mapperInterface:Mapper 接口类型,也是当前 MapperProxy 关联的代理对象实现的接口类型。
  • methodCache:用于缓存 MapperMethodInvoker 对象的集合。methodCache 中的 key 是 Mapper 接口中的方法,value 是该方法对应的 MapperMethodInvoker 对象。
  • lookupConstructor:针对 JDK 8 中的特殊处理,该字段指向了 MethodHandles.Lookup 的构造方法。
  • privateLookupInMethod:除了 JDK 8 之外的其他 JDK 版本会使用该字段,该字段指向 MethodHandles.privateLookupIn() 方法。

这里涉及 MethodHandle 的内容,所以下面我们就来简单介绍一下 MethodHandle 的基础知识点。

3.1 MethodHandle 简介

从 Java 7 开始,除了反射之外,在 java.lang.invoke 包中新增了 MethodHandle 这个类,它的基本功能与反射中的 Method 类似,但它比反射更加灵活。反射是 Java API 层面支持的一种机制,而 MethodHandle 则是由 JVM 实现的机制。相比之下,MethodHandle 更轻量级,性能也比反射更好。

使用 MethodHandle 进行方法调用的时候,往往会涉及下面几个核心步骤:

  1. 创建 MethodType 对象,确定方法的签名,这个签名会涉及方法参数及返回值的类型;
  2. MethodHandles.Lookup 这个工厂对象中,根据方法名称以及上面创建的 MethodType 查找对应 MethodHandle 对象;
  3. MethodHandle 绑定到一个具体的实例对象;
  4. 调用 MethodHandle.invoke()invokeWithArguments()invokeExact() 等方法,完成方法调用。

下面是 MethodHandle 的一个简单示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MethodHandleDemo {
  // 定义一个 sayHello() 方法
  public String sayHello(String s) {
    return "Hello, " + s;
  }
  public static void main(String[] args) throws Throwable {
    // 初始化 MethodHandleDemo 实例
    MethodHandleDemo subMethodHandleDemo = new SubMethodHandleDemo();
    // 定义 sayHello() 方法的签名,第一个参数是方法的返回值类型,第二个参数是方法的参数列表
    MethodType methodType = MethodType.methodType(String.class, String.class);
    // 根据方法名和 MethodType 在 MethodHandleDemo 中查找对应的 MethodHandle
    MethodHandle methodHandle = MethodHandles.lookup()
      .findVirtual(MethodHandleDemo.class, "sayHello", methodType);
    // 将 MethodHandle 绑定到一个对象上,然后通过 invokeWithArguments() 方法传入实参并执行
    System.out.println(methodHandle.bindTo(subMethodHandleDemo)
    .invokeWithArguments("MethodHandleDemo"));
    // 下面是调用 MethodHandleDemo 对象(即父类)的方法
    MethodHandleDemo methodHandleDemo = new MethodHandleDemo();
    System.out.println(methodHandle.bindTo(methodHandleDemo)
      .invokeWithArguments("MethodHandleDemo"));
  }
  public static class SubMethodHandleDemo extends MethodHandleDemo{
    // 定义一个 sayHello() 方法
    public String sayHello(String s) {
      return "Sub Hello, " + s;
    }
  }
}

MethodHandle 调用方法的时候,也是支持多态的,在通过 MethodHandle.bindTo() 方法绑定到某个实例对象的时候,在 bind 过程中会进行类型检查等一系列检查操作。

通过上面这个示例我们可以看出,使用 MethodHandle 实现反射的效果,更像我们平时通过 Java 代码生成的字节码,例如,在字节码中可以看到创建的方法签名(MethodType)、方法的具体调用方式(findStatic()findSpecial()findVirtual() 等方法)以及类型的隐式转换。

3.2 MethodProxy 中的代理逻辑

介绍完 MethodHandle 的基础之后,我们回到 MethodProxy 继续分析。

MapperProxy.invoke() 方法是代理对象执行的入口,其中会拦截所有非 Object 方法,针对每个被拦截的方法,都会调用 cachedInvoker() 方法获取对应的 MapperMethod 对象,并调用其 invoke() 方法执行代理逻辑以及目标方法。

cachedInvoker() 方法中,首先会查询 methodCache 缓存,如果查询的方法为 default 方法,则会根据当前使用的 JDK 版本,获取对应的 MethodHandle 并封装成 DefaultMethodInvoker 对象写入缓存;如果查询的方法是非 default 方法,则创建 PlainMethodInvoker 对象写入缓存。

cachedInvoker() 方法的具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (m.isDefault()) {
        // 针对 default 方法的处理
        try {
          // 这里根据 JDK 版本的不同,获取方法对应的 MethodHandle 的方式也有所不同
          // 在 JDK 8 中使用的是 lookupConstructor 字段,而在 JDK 9 中使用的是
          // privateLookupInMethod 字段。获取到 MethodHandle 之后,会使用 
          // DefaultMethodInvoker 进行封装
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return new DefaultMethodInvoker(getMethodHandleJava9(method));
          }
        } // catch...
      } else {
        // 对于其他方法,会创建 MapperMethod 并使用 PlainMethodInvoker 封装
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    }
    );
  } // catch...
}

其中使用到的 DefaultMethodInvokerPlainMethodInvoker 都是 MapperProxy 的内部接口 MapperMethodInvoker 的实现,如下图所示:

MapperMethodInvoker 接口继承关系图

DefaultMethodInvoker.invoke() 方法中,会通过底层维护的 MethodHandle 完成方法调用,核心实现如下:

1
2
3
4
5
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  // 首先将 MethodHandle 绑定到一个实例对象上,然后调用 invokeWithArguments() 方法执行目标方法
  return methodHandle.bindTo(proxy).invokeWithArguments(args);
}

PlainMethodInvoker.invoke() 方法中,会通过底层维护的 MapperMethod 完成方法调用,其核心实现如下:

1
2
3
4
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args);
}

4 MapperMethod

通过对 MapperProxy 的分析我们知道,MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法,其核心字段也围绕这两方面的内容展开。

1
2
3
4
public class MapperMethod {
  private final SqlCommand command;
  private final MethodSignature method;
}

4.1 SqlCommand

MapperMethod 的第一个核心字段是 commandMapperMethod$SqlCommand 类型),其中维护了关联 SQL 语句的相关信息。在 MapperMethod$SqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERTUPDATEDELETESELECTFLUSH 五种:

1
2
3
4
5
6
public class MapperMethod {
  public static class SqlCommand {
    private final String name;
    private final SqlCommandType type;
  }
}
1
2
3
public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
}

下面我们就来看看 SqlCommand 如何查找 Mapper 接口中一个方法对应的 SQL 语句的信息,该逻辑在 SqlCommand 的构造方法中实现,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  // 获取 Mapper 接口中对应的方法名称
  final String methodName = method.getName();
  // 获取 Mapper 接口的类型
  final Class<?> declaringClass = method.getDeclaringClass();
  // 将 Mapper 接口名称和方法名称拼接起来作为 SQL 语句唯一标识,
  // 到 Configuration 这个全局配置对象中查找 SQL 语句
  // MappedStatement 对象就是 Mapper.xml 配置文件中一条 SQL 语句解析之后得到的对象
  MappedStatement ms = resolveMappedStatement(mapperInterface,
   methodName, declaringClass, configuration);
  if (ms == null) {
    // 针对 @Flush 注解的处理
    if (method.getAnnotation(Flush.class) != null) {
      name = null;
      type = SqlCommandType.FLUSH;
    } else { // 没有 @Flush 注解,会抛出异常
        throw new BindingException("Invalid bound statement (not found): "
          + mapperInterface.getName() + "." + methodName);
    }
  } else {
    // 记录 SQL 语句唯一标识
    name = ms.getId();
    // 记录 SQL 语句的操作类型,UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
    type = ms.getSqlCommandType();
    if (type == SqlCommandType.UNKNOWN) {
      throw new BindingException("Unknown execution method for: " + name);
    }
  }
}

这里调用的 SqlCommand.resolveMappedStatement() 方法不仅会尝试根据 SQL 语句的唯一标识从 Configuration 全局配置对象中查找关联的 MappedStatement 对象,还会尝试顺着 Mapper 接口的继承树进行查找,直至查找成功为止。具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private MappedStatement resolveMappedStatement(
    Class<?> mapperInterface, String methodName,
    Class<?> declaringClass, Configuration configuration) {
  // 将 Mapper 接口名称和方法名称拼接起来作为 SQL 语句唯一标识
  String statementId = mapperInterface.getName() + "." + methodName;
  // 检测 Configuration 中是否包含相应的 MappedStatement 对象
  if (configuration.hasStatement(statementId)) {
    return configuration.getMappedStatement(statementId);
  } else if (mapperInterface.equals(declaringClass)) {
    // 如果方法就定义在当前接口中,则证明没有对应的 SQL 语句,返回 null
    return null;
  }
  // 如果当前检查的 Mapper 接口 (mapperInterface) 中不是定义该方法的接口 (declaringClass),
  // 则会从 mapperInterface 开始,沿着继承关系向上查找递归每个接口,
  // 查找该方法对应的 MappedStatement 对象
  for (Class<?> superInterface : mapperInterface.getInterfaces()) {
    if (declaringClass.isAssignableFrom(superInterface)) {
      MappedStatement ms = resolveMappedStatement(
        superInterface, methodName,
        declaringClass, configuration);
      if (ms != null) {
        return ms;
      }
    }
  }
  return null;
}

4.2 MethodSignature

MapperMethod 的第二个核心字段是 method 字段(MapperMethod$MethodSignature 类型),其中维护了 Mapper 接口中方法的相关信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Boolean returnsMany;
Boolean returnsMap;
Boolean returnsVoid;
Boolean returnsCursor;
Boolean returnsOptional;
Class<?> returnType;
String mapKey;
Integer resultHandlerIndex;
Integer rowBoundsIndex;
ParamNameResolver paramNameResolver;
  • 首先是 Mapper 接口方法返回值的相关信息,涉及下面七个字段:
    • returnsManyreturnsMapreturnsVoidreturnsCursorreturnsOptionalboolean 类型):用于表示方法返回值是否为 Collection 集合或数组、Map 集合、voidCursorOptional 类型。
    • returnTypeClass<?> 类型):方法返回值的具体类型。
    • mapKeyString 类型):如果方法的返回值为 Map 集合,则通过 mapKey 字段记录了作为 key 的列名。mapKey 字段的值是通过解析方法上的 @MapKey 注解得到的。
  • 接下来是与 Mapper 接口方法的参数列表相关的三个字段:
    • resultHandlerIndexInteger 类型):记录了 Mapper 接口方法的参数列表中 ResultHandler 类型参数的位置。
    • rowBoundsIndexInteger 类型):记录了 Mapper 接口方法的参数列表中 RowBounds 类型参数的位置。
    • paramNameResolverParamNameResolver 类型):用来解析方法参数列表的工具类。

在上述字段中,需要着重讲解的是 ParamNameResolver 这个解析方法参数列表的工具类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ParamNameResolver {
  private final boolean useActualParamName;
  /**
  * <p>
  * 键是索引,值是参数的名称。<br> 如果指定,名称从{@link Param}获取。
  * 当未指定 {@link Param} 时,使用参数索引。
  * 请注意,当该方法具有特殊参数(即 {@link RowBounds} 或 {@link ResultHandler})时,此索引可能与实际索引不同。
  * </p>
  * <ul>
  * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
  * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
  * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
  * </ul>
  */
  private final SortedMap<Integer, String> names;
}

ParamNameResolver 中有一个 names 字段(SortedMap<Integer, String> 类型)记录了各个参数在参数列表中的位置以及参数名称,其中:

  • key 是参数在参数列表中的位置索引;
  • value 为参数的名称。
警告

我们可以通过 @Param 注解指定一个参数名称,如果没有特别指定,则默认使用参数列表中的变量名称作为其名称,这与 ParamNameResolveruseActualParamName 字段相关。useActualParamName 是一个全局配置。

如果我们将 useActualParamName 配置为 falseParamNameResolver 会使用参数的下标索引作为其名称。另外,names 集合会跳过 RowBounds 类型以及 ResultHandler 类型的参数,如果使用下标索引作为参数名称,在 names 集合中就会出现 KV 不一致的场景。例如下图就很好地说明了这种不一致的场景,其中 saveCustomer(long id, String name, RowBounds bounds, String addr) 方法对应的 names 集合为 {{0, 0}, {1, "1"}, {2, "3"}}:

names 集合中 KV 不一致示意图

从图中可以看到,由于 RowBounds 参数的存在,第四个参数在 names 集合中的 KV 出现了不一致(即 key = 2value = "3" 不一致)。

完成 names 集合的初始化之后,我们再来看如何从 names 集合中查询参数名称,该部分逻辑在 ParamNameResolver.getNamedParams() 方法中,它会将 Mapper 接口方法的实参与 names 集合中记录的参数名称相关联,其核心逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public Object getNamedParams(Object[] args) {
  // 获取方法中非特殊类型 (RowBounds 类型和 ResultHandler 类型)的参数个数
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    return null; // 方法没有非特殊类型参数,返回 null 即可
  } else if (!hasParamAnnotation && paramCount == 1) {
    // 方法参数列表中没有使用 @Param 注解,且只有一个非特殊类型参数
    Object value = args[names.firstKey()];
    return wrapToMapIfCollection(value, useActualParamName ? names.get(names.firstKey()) : null);
  } else {
    // 处理存在 @Param 注解或是存在多个非特殊类型参数的场景
    // param 集合用于记录了参数名称与实参之间的映射关系
    // 这里的 ParamMap 继承了 HashMap,与 HashMap 的唯一不同是:
    // 向 ParamMap 中添加已经存在的 key 时,会直接抛出异常,而不是覆盖原有的 Key
    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()]);
      // 同时,为参数创建 "param + 索引" 格式的默认参数名称,具体格式为:param1, param2 等,
      // 将 "param + 索引" 的默认参数名称与实参的映射关系也保存到 param 集合中
      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;
  }
}

了解了 ParamNameResolver 的核心功能之后,我们回到 MethodSignature 继续分析,在其构造函数中会解析方法中的返回值、参数列表等信息,并初始化前面介绍的核心字段,这里也会使用到前面介绍的 ParamNameResolver 工具类。下面是 MethodSignature 构造方法的核心实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
  // 通过 TypeParameterResolver 工具类解析方法的返回值类型,初始化 returnType 字段值
  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();
  }
  // 根据返回值类型,初始化 returnsVoid、returnsMany、returnsCursor、
  // returnsMap、returnsOptional 这五个与方法返回值类型相关的字段
  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);
  // 如果返回值为 Map 类型,则从方法的 @MapKey 注解中获取 Map 中为 key 的字段名称
  this.mapKey = getMapKey(method);
  this.returnsMap = this.mapKey != null;
  // 解析方法中 RowBounds 类型参数以及 ResultHandler 类型参数的下标索引位置,
  // 初始化 rowBoundsIndex 和 resultHandlerIndex 字段
  this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
  this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
  // 创建 ParamNameResolver 工具对象,在创建 ParamNameResolver 对象的时候,
  // 会解析方法的参数列表信息
  this.paramNameResolver = new ParamNameResolver(configuration, method);
}

在初始化过程中,我们看到会调用 getUniqueParamIndex() 方法查找目标类型参数的下标索引位置,其核心原理就是遍历方法的参数列表,逐个匹配参数的类型是否为目标类型,如果匹配成功,则会返回当前参数的下标索引。getUniqueParamIndex() 方法的具体实现比较简单,这里就不再展示了。

4.3 execute() 方法

分析完 MapperMethod 中的几个核心内部类,我们回到 MapperMethod 继续介绍。

execute() 方法是 MapperMethod 中最核心的方法之一。execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作,其核心实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public Object execute(SqlSession sqlSession, Object[] args) {
	Object result;
	switch (command.getType()) { // 判断 SQL 语句的类型
		case INSERT: {
      // 通过 ParamNameResolver.getNamedParams() 方法将方法的实参与
      // 参数的名称关联起来
			Object param = method.convertArgsToSqlCommandParam(args);
      // 通过 SqlSession.insert() 方法执行 INSERT 语句,
      // 在 rowCountResult() 方法中,会根据方法的返回值类型对结果进行转换
			result = rowCountResult(sqlSession.insert(command.getName(), param));
			break;
		}
		case UPDATE: {
			Object param = method.convertArgsToSqlCommandParam(args);
      // 通过 SqlSession.update() 方法执行 UPDATE 语句
			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()) {
        // 如果方法返回值为 void,且参数中包含了 ResultHandler 类型的实参,
        // 则查询的结果集将会由 ResultHandler 对象进行处理
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // executeForMany() 方法处理返回值为集合或数组的场景
        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("...");
	}
	return result;
}

execute() 方法中,对于 INSERTUPDATEDELETE 三类 SQL 语句的返回结果,都会通过 rowCountResult() 方法处理。我们知道,上述三种类型的 SQL 语句的执行结果是一个数字,多数场景中代表了 SQL 语句影响的数据行数(注意,这个返回值的具体含义根据 MySQL 的配置有所变化),rowCountResult() 方法会将这个 int 值转换成 Mapper 接口方法的返回值,具体规则如下:

  • Mapper 方法返回值为 void,则忽略 SQL 语句的 int 返回值,直接返回 null;
  • Mapper 方法返回值为 intInteger 类型,则将 SQL 语句返回的 int 值直接返回;
  • Mapper 方法返回值为 longLong 类型,则将 SQL 语句返回的 int 值转换成 long 类型之后返回;
  • Mapper 方法返回值为 booleanBoolean 类型,则将 SQL 语句返回的 int 值与 0 比较大小,并将比较结果返回。

接下来看 execute() 方法针对 SELECT 语句查询到的结果集的处理。

  • 如果在方法参数列表中有 ResultHandler 类型的参数存在,则会使用 executeWithResultHandler() 方法完成查询,底层依赖的是 SqlSession.select() 方法,结果集将会交由传入的 ResultHandler 对象进行处理。
  • 如果方法返回值为集合类型或是数组类型,则会调用 executeForMany() 方法,底层依赖 SqlSession.selectList() 方法进行查询,并将得到的 List 转换成目标集合类型。
  • 如果方法返回值为 Map 类型,则会调用 executeForMap() 方法,底层依赖 SqlSession.selectMap() 方法完成查询,并将结果集映射成 Map 集合。
  • 针对 Cursor 以及 Optional 返回值的处理,也是依赖的 SqlSession 的相关方法完成查询的,这里不再展开。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容