通常,我们会为每个 Mapper.xml 配置文件创建一个对应的 Mapper 接口,并且无须提供这个接口的实现,在执行 SQL 语句时直接调用这个 Mapper 对象的方法执行即可。那 MyBatis 是如何通过这个 Mapper 接口来执行相应的 SQL 语句的呢?实际上,在 MyBatis 中,实现 Mapper 接口与 Mapper.xml 配置文件映射功能的是 binding 模块,其涉及的核心类如下图所示:
image.png

MapperRegistry

MapperRegistry 是 MyBatis 初始化过程中构造的一个对象,主要作用就是统一维护 Mapper 接口以及这些 Mapper 的代理对象工厂。在 MyBatis 初始化时,所有配置信息会被解析成相应的对象记录到 Configuration 对象中。其中,Configuration 的 mapperRegistry 属性就记录了当前使用的 MapperRegistry 对象。

下面我们先来看 MapperRegistry 中的核心字段:

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

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

  1. public <T> void addMapper(Class<T> type) {
  2. // 是否是接口
  3. if (type.isInterface()) {
  4. // 是否已经存在
  5. if (hasMapper(type)) {
  6. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  7. }
  8. boolean loadCompleted = false;
  9. try {
  10. // 添加到knownMappers集合中
  11. knownMappers.put(type, new MapperProxyFactory<T>(type));
  12. // Mapper对应的XML解析和注解处理
  13. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  14. parser.parse();
  15. loadCompleted = true;
  16. } finally {
  17. if (!loadCompleted) {
  18. knownMappers.remove(type);
  19. }
  20. }
  21. }
  22. }

MapperProxyFactory

在需要执行某 SQL 语句时,会先调用 MapperRegistry 的 getMapper() 方法获取实现了 Mapper 接口的代理对象,然后通过代理对象执行具体的操作。这里会先拿到之前创建的 MapperProxyFactory 工厂对象,并调用其 newInstance() 方法创建 Mapper 接口的代理对象。底层是通过 JDK 动态代理的方式生成代理对象的,如下代码所示,这里使用的 InvocationHandler 实现是 MapperProxy。

  1. public class MapperProxyFactory<T> {
  2. @SuppressWarnings("unchecked")
  3. protected T newInstance(MapperProxy<T> mapperProxy) {
  4. return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  5. }
  6. public T newInstance(SqlSession sqlSession) {
  7. final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  8. return newInstance(mapperProxy);
  9. }
  10. }

MapperProxy

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

  1. // 记录了当前MapperProxy关联的SqlSession对象, 用于访问数据库
  2. private final SqlSession sqlSession;
  3. // Mapper接口类型,也是当前MapperProxy关联的代理对象实现的接口类型
  4. private final Class<T> mapperInterface;
  5. // key是Mapper接口中的方法对应的Method对象,value是该方法对应的MapperMethod对象
  6. // MapperMethod对象会完成参数转换以及SQL语句的执行功能
  7. private final Map<Method, MapperMethod> methodCache;

MapperProxy 中的 invoke() 方法是代理对象执行的主要逻辑,其实现如下:

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. // 如果是Object中定义的方法,则直接调用,无需代理
  4. if (Object.class.equals(method.getDeclaringClass())) {
  5. return method.invoke(this, args);
  6. } else if (isDefaultMethod(method)) {
  7. // 针对Java7以上版本对动态类型语言的支持
  8. return invokeDefaultMethod(proxy, method, args);
  9. }
  10. } catch (Throwable t) {
  11. throw ExceptionUtil.unwrapThrowable(t);
  12. }
  13. // 从缓冲中获取MapperMethod对象,如果缓冲中没有,则创建新的MapperMethod对象并添加到缓存中
  14. final MapperMethod mapperMethod = cachedMapperMethod(method);
  15. // 执行SQL语句
  16. return mapperMethod.execute(sqlSession, args);
  17. }

MapperMethod

MapperMethod 中封装了 Mapper 接口中对应方法的信息,以及对应 SQL 语句的信息。我们可以将 MapperMethod 看作是连接 Mapper 接口与 Mapper.xml 配置文件中定义的 SQL 语句的桥梁。其核心字段如下代码所示:

  1. // 记录了SQL语句的名称和类型
  2. private final SqlCommand command;
  3. // Mapper接口中对应方法的相关信息
  4. private final MethodSignature method;

1. SqlCommand

SqlCommand 是 MapperMethod 中定义的内部类,它使用 name 字段记录了 SQL 语句的名称,使用 type 字段(SqlCommandType 枚举类型)记录了 SQL 语句的类型。SqlCommand 的构造方法会初始化 name 字段和 type 字段,代码如下:

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

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

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

2. MethodSignature

MethodSignature 也是 MapperMethod 中定义的内部类,其维护了 Mapper 接口中方法的相关信息。

  1. // 表示方法返回值是否为Collection集合或数组
  2. private final boolean returnsMany;
  3. // 表示方法返回值是否为Map集合
  4. private final boolean returnsMap;
  5. // 表示方法返回值是否为void
  6. private final boolean returnsVoid;
  7. // 表示方法返回值是否为Cursor
  8. private final boolean returnsCursor;
  9. // 方法返回值的具体类型
  10. private final Class<?> returnType;
  11. // 如果方法的返回值为Map集合,则通过mapKey字段记录了作为key的列名。mapKey字段的值是通过解析方法上的@MapKey注解得到的
  12. private final String mapKey;
  13. // 记录了Mapper接口方法的参数列表中ResultHandler类型参数的位置
  14. private final Integer resultHandlerIndex;
  15. // 记录了Mapper接口方法的参数列表中RowBounds类型参数的位置
  16. private final Integer rowBoundsIndex;
  17. // 用来解析方法参数列表的工具类
  18. private final ParamNameResolver paramNameResolver;

在上述字段中,需要着重讲解的是 ParamNameResolver 这个解析方法参数列表的工具类。在 ParamNameResolver 中有一个 names 字段(SortedMap类型)记录了各个参数在参数列表中的位置以及参数名称,其中 key 是参数在参数列表中的位置索引,value 为参数的名称。我们可通过 @Param 注解指定一个参数名称,如果没有指定 @Param 注解,则默认使用变量名称作为其名称。

另外,names 集合会跳过 RowBounds 类型以及 ResultHandler 类型的参数,如果使用下标索引作为参数名称,在 names 集合中就会出现 KV 不一致的场景。例如下图就很好地说明了这种不一致的场景,其中 saveCustomer(long id, String name, RowBounds bounds, String address) 方法对应的 names 集合为 {{0, “0”}, {1, “1”}, {2, “3”}}。由于 RowBounds 参数的存在,第四个参数在 names 集合中的 KV 出现了不一致,即 key = 2 与 value = “3” 不一致。
image.png
在 ParamNameResolver 的构造方法中,会通过反射的方式读取 Mapper 接口中对应方法的信息,并初始化 names 集合,具体实现代码如下:

  1. public ParamNameResolver(Configuration config, Method method) {
  2. // 获取参数列表中每个参数的类型
  3. final Class<?>[] paramTypes = method.getParameterTypes();
  4. // 获取参数列表上的注解
  5. final Annotation[][] paramAnnotations = method.getParameterAnnotations();
  6. // 该集合用于记录参数索引与参数名称的对应关系
  7. final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
  8. int paramCount = paramAnnotations.length;
  9. for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
  10. // 如果参数是RowBounds或ResultHandler类型,则跳过该参数
  11. if (isSpecialParameter(paramTypes[paramIndex])) {
  12. continue;
  13. }
  14. String name = null;
  15. // 遍历该参数对应的注解集合
  16. for (Annotation annotation : paramAnnotations[paramIndex]) {
  17. if (annotation instanceof Param) {
  18. hasParamAnnotation = true;
  19. // 获取@Param注解指定的参数名称
  20. name = ((Param) annotation).value();
  21. break;
  22. }
  23. }
  24. if (name == null) {
  25. // 获取原始参数名称,Configuration中的useActualParamName默认为true
  26. if (config.isUseActualParamName()) {
  27. name = getActualParamName(method, paramIndex);
  28. }
  29. if (name == null) {
  30. // 获取参数下标索引作为参数名称
  31. name = String.valueOf(map.size());
  32. }
  33. }
  34. map.put(paramIndex, name);
  35. }
  36. // 保存到names集合中
  37. names = Collections.unmodifiableSortedMap(map);
  38. }

names 集合主要在 ParamNameResolver 的 getNamedParams() 方法中使用,该方法接收的参数是用户传入的实参列表,并将实参与其对应名称进行关联。

了解了 ParamNameResolver 的核心功能后,我们回到 MethodSignature 继续分析,在构造 MethodSignature 时会解析方法中的返回值、参数列表等信息,并会初始化上面介绍的几个核心字段。

3. 方法执行

介绍完 MapperMethod 中定义的内部类,我们回到 MapperMethod 继续分析。MapperMethod 中最核心的方法是 execute() 方法,它会根据 SQL 语句的类型调用 SqISession 对应的方法完成数据库操作。SqlSession 是 MyBatis 的核心组件之一,其具体实现我们后面会详细介绍。MapperMethod.execute() 方法的具体实现如下:

  1. public Object execute(SqlSession sqlSession, Object[] args) {
  2. Object result;
  3. // 根据SQL语句的类型调用SqlSession对应的方法
  4. switch (command.getType()) {
  5. case INSERT: {
  6. // 使用ParamNameResolver处理args[]数组(用户传入的实参列表),将用户传入的实参与指定参数名称关联起来
  7. Object param = method.convertArgsToSqlCommandParam(args);
  8. // 调用SqlSession的insert方法,rowCountResult方法会根据method字段中记录的方法的返回值类型对结果进行转换
  9. result = rowCountResult(sqlSession.insert(command.getName(), param));
  10. break;
  11. }
  12. // UPDATE和DELETE类型的SQL语句的处理与INSERT类型的SQL语句相似
  13. case UPDATE:
  14. case DELETE:
  15. ......
  16. case SELECT:
  17. // 处理返回值为void且ResultSet通过ResultHandler处理的方法
  18. if (method.returnsVoid() && method.hasResultHandler()) {
  19. executeWithResultHandler(sqlSession, args);
  20. result = null;
  21. } else if (method.returnsMany()) {
  22. // 处理返回值为集合或数组的方法
  23. result = executeForMany(sqlSession, args);
  24. } else if (method.returnsMap()) {
  25. // 处理返回值为Map的方法
  26. result = executeForMap(sqlSession, args);
  27. } else if (method.returnsCursor()) {
  28. // 处理返回值为Cursor的方法
  29. result = executeForCursor(sqlSession, args);
  30. } else {
  31. // 处理返回值为单一对象的方法
  32. Object param = method.convertArgsToSqlCommandParam(args);
  33. result = sqlSession.selectOne(command.getName(), param);
  34. }
  35. break;
  36. ......
  37. }
  38. return result;
  39. }

在 execute() 方法中,当执行 INSERT、UPDATE、DELETE 这三种类型的 SQL 语句时,其执行结果都需要经过 rowCountResult() 方法处理。比如:SqISession 中的 insert() 方法返回的是 int 值,rowCountResult() 方法会将该 int 值转换成 Mapper 接口中对应方法的返回值。多数场景中这个 int 值代表了 SQL 语句影响的数据行数,但这个返回值的具体含义会根据 MySQL 的配置有所变化。