MyBatis中Mapper方法的执行过程

MyBatis中Mapper对象的创建过程一文中,在创建mapper的动态代理对象时指定的InvocationHandlerMapperFactory类型的。那么所有mapper接口中的方法都会经过MapperFactory类的invoke方法,该方法就是分析Mapper方法也就是整个MyBatis运行时的入口。


整个MyBatis执行过程可以拆分为下面几个部分:

  • 统一拦截层;
  • SqlSession层;
  • Executor层;
  • JDBC层;

上面是我个人自己的总结,不是MyBatis的官方说法。接下来我一个个步骤进行介绍。

统一拦截层

MapperProxy

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/binding/MapperProxy.java
/*
 * MyBatis中所有Mapper的接口方法会被该方法拦截
 */
@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 {
      /*
       * 先获取invoker对象,再调用。
       * 如果缓存中不存在invoker对象,那么新建一个并缓存起来。
       */
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      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和invoker
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

这里主要是获取或创建了方法调用器对象,主要考虑PlainMethodInvoker这种情况。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/binding/MapperProxy.java
private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;

  public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    /*
     * 执行方法
     */
    return mapperMethod.execute(sqlSession, args);
  }
}

这里仅是简单分装,调用了MapperMethodexecute方法。

MapperMethod

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/binding/MapperMethod.java
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  // 创建SQL命令对象,内部会判断SQL的类型
  this.command = new SqlCommand(config, mapperInterface, method);
  this.method = new MethodSignature(config, mapperInterface, method);
}
/*
 * MyBatis中mapper方法的核心实现
 */
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  /*
   * 根据命令的类型,执行不同的操作。
   * 这些操作都是基于SqlSession来执行的。
   */
  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()) { // 返回map
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) { // 返回游标
        result = executeForCursor(sqlSession, args);
      } else { // 其他情况
        /*
         * 通过参数名称解析器来处理参数,返回的类型之所以用Object,是因为会根据参数情况返回不同的类型,如Collection,Map等
         * 既然这里是Object类型的,后续执行时是怎么使用param对象的?怎么知道param是哪种具体类型的?
         */
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行查询
        result = sqlSession.selectOne(command.getName(), param);
        // 方法返回类型为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());
  }
  // 如果底层返回结果为null,但是mapper方法的返回值是基础类型但不为void,则抛出异常
  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;
}

该方法中针对不同类型的SQL语句进行了不同的处理,各种情况只是中间处理过程不一样,最终执行到底层时都是类似的,所以本文选用一般的case SELECT的情况,即最后一个else。 这种情况先是对参数进行了处理,因为有各种情况的参数,所以这里同一封装为Object;然后调用了SqlSessionselectOne方法。

ParamNameResolver

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/binding/MapperMethod.java
public Object convertArgsToSqlCommandParam(Object[] args) {
  // 获取参数名-值映射
  return paramNameResolver.getNamedParams(args);
}
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);
}

MethodSignature构造方法中创建了ParamNameResolver对象,这个构造过程还是比较重要的,对参数名称进行了处理。

v3.x
ParamNameResolver
ParamNameUtil
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java
public ParamNameResolver(Configuration config, Method method) {
  // 判断是否使用实际的参数名称,默认为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++) {
    // 判断是不是特殊参数,如果是则跳过(里面判断了两种类型,大部分参数都不会是这里的特殊参数)
    if (isSpecialParameter(paramTypes[paramIndex])) {
      // skip special parameters
      continue;
    }
    String name = null;
    // 遍历方法参数的注解
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      // 如果是@Param注解
      if (annotation instanceof Param) {
        hasParamAnnotation = true;
        // 获取通过注解配置的参数名称
        name = ((Param) annotation).value();
        break;
      }
    }
    // 没有使用@Param注解,或者使用了注解但没有指定参数名称
    if (name == null) {
      // @Param was not specified.
      // 如果使用参数名,则获取参数名称
      if (useActualParamName) {
        name = getActualParamName(method, paramIndex);
      }
      // 如果不使用实际参数名,那么使用参数序号作为参数名
      if (name == null) {
        // use the parameter index as the name ("0", "1", ...)
        // gcode issue #71
        name = String.valueOf(map.size());
      }
    }
    // 保存参数名称
    map.put(paramIndex, name);
  }
  names = Collections.unmodifiableSortedMap(map);
}
private String getActualParamName(Method method, int paramIndex) {
  return ParamNameUtil.getParamNames(method).get(paramIndex);
}
java
mybatis-3/src/main/java/org/apache/ibatis/reflection/ParamNameUtil.java
public class ParamNameUtil {
  public static List<String> getParamNames(Method method) {
    return getParameterNames(method);
  }

  public static List<String> getParamNames(Constructor<?> constructor) {
    return getParameterNames(constructor);
  }

  private static List<String> getParameterNames(Executable executable) {
    return Arrays.stream(executable.getParameters()).map(Parameter::getName).collect(Collectors.toList());
  }

  private ParamNameUtil() {
    super();
  }
}

可以分为三种情况:

  • 使用了@Param注解且配置了参数名称,则获取注解中的名称;
  • 如果允许使用实际参数名称,获取形参名称;
  • 如果不允许使用实际参数名称,则使用数字序号(从0开始,字符串形式)作为参数名称;

这里是是否允许使用实际参数名称是Configuration中的useActualParamName属性来控制的,默认是true。实际开发中一般也不会修改该属性。 在获取实参时,是通过JDK的反射相关类来实现的。该实现实际是从.class文件中获取的参数名称,如果编译时没有在.class文件中保留参数名称,那么这里会返回"argN"格式的名称,其中"N"是参数序号(从0开始)。 默认情况下,javac编译器是不会写入参数实际名称的,以此来减少.class文件的大小;如果需要保留参数名称,可以指定编译参数-parameters

接着看ParamNameReolvergetNamedParams方法。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    return null;
  } else if (!hasParamAnnotation && paramCount == 1) { // 如果参数个数为1, 而且没有@Param注解
    Object value = args[names.firstKey()];
    // 根据参数值类型建立其他的名称映射
    return wrapToMapIfCollection(value, useActualParamName ? names.get(names.firstKey()) : null);
  } else {
    // 创建参数名和值的映射
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    // 遍历names
    for (Map.Entry<Integer, String> entry : names.entrySet()) {
      // 添加参数名-值键值对
      param.put(entry.getValue(), args[entry.getKey()]);
      // add generic param names (param1, param2, ...)
      // 创建通用参数名,即前缀(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;
  }
}
public static Object wrapToMapIfCollection(Object object, String actualParamName) {
  // 如果是Collection及其子类型
  if (object instanceof Collection) {
    ParamMap<Object> map = new ParamMap<>();
    map.put("collection", object);
    if (object instanceof List) { // 如果是list,则同时添加这种参数名映射
      map.put("list", object);
    }
    // 实际参数名映射
    Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
    return map;
  } else if (object != null && object.getClass().isArray()) { // 如果是数组类型
    ParamMap<Object> map = new ParamMap<>();
    map.put("array", object);
    Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
    return map;
  }
  return object;
}

在使用MyBatis开发时,经常遇到找不到参数映射或者参数映射错误的情况,尤其是mapper方法有多个参数的时候。理解了这部分代码逻辑,会很大程度解决这个问题。当然使用@Param注解也可以在不理解的情况下进行正常开发,但多写一个注解总是麻烦的,从实际来看,大部分开发都不会写。

该方法逻辑可以分为三种情况:

  • 没有参数:直接返回null
  • 只有一个参数,且没有指定@Param注解:会调用wrapToMapIfCollection方法来进行一些特殊的参数名称映射,包括“collection”, “list”或“array”;
  • 其他情况:添加实际名称,另外还会添加通用的参数名称,即“paramN”格式,其中“N”是参数序号(从1开始)。

SqlSession

上面说到,MapperMethodexecute方法会调用SqlSession中的方法,这里以selectOne方法为例继续分析。

SqlSession是个接口,具体是什么类型的呢?这就要找到创建SqlSession对象的地方了,实际上,该对象是在创建MapperProxy实例时传进来的。这就要回到MapperFactoryBean中的getObject方法了,SqlSession对象就是从这里开始传入MapperProxy中的,即MapperFactoryBean中的属性sqlSessionTemplate,而在Spring Boot中,此对象则是MyBatisAutoConfiguration中注入的SqlSessionTemplate对象。这个过程在MyBatis中Mapper对象的创建过程一文中已详细介绍,本文不再赘述。

所以SqlSession层使用的对象是SqlSessionTemplate对象,这个template对象其实是为了和Spring的事务管理机制进行适配做的一层封装,而这个类本身也位于mybatis-spring模块。

SqlSessionTemplate

v3.x
java
mybatis-spring/src/main/java/org/mybatis/spring/SqlSessionTemplate.java
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  // 创建SqlSession的代理对象,实际的操作会委托给此对象处理
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      /*
       * 获取sqlSession,与Spring的事务机制适配
       * 这里的sqlSessionFactory是DefaultSqlSessionFactory类型的,
       *  参考SqlSessionFactoryBean#buildSqlSessionFactory方法的结尾,也就是SqlSessionFactoryBuilder的build方法。
       * 这里的SqlSessionFactory是DefaultSqlSessionFactory,所以这里创建的是DefaultSqlSession类型的对象。
       */
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 调用目标方法,通过上面获取的sqlSession来反射调用
        Object result = method.invoke(sqlSession, args);
        // 如果当前操作不在事务中,则立马提交
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          // 关闭sqlSession
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // 通过代理执行,此代理用于适配spring的事务管理,参考上面构造方法中该变量的初始化
    return this.sqlSessionProxy.selectOne(statement, parameter);
  }

实际上,SqlSessionTemplate中的很多从SqlSession实现的操作都是交给代理对象来处理的,而在SqlSessionInterceptor中,是通过SqlSessionUtils来获取的SqlSession对象。

v3.x
java
mybatis-spring/src/main/java/org/mybatis/spring/SqlSessionUtils.java
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

  // 通过spring的事务同步管理器来获取sqlSession
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

  LOGGER.debug(() -> "Creating a new SqlSession");
  // 如果上面没有获取到,则新建一个sqlSession
  session = sessionFactory.openSession(executorType);

  // 添加到线程本地存储中,避免后续重复创建
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

可以看到,MyBatis是通过spring的事务管理机制来管理SqlSession对象,如果不存在则调用SqlSessionFactoryopenSession方法来新建。同样回到MyBatisAutoConfiguration中,这里注册了SqlSessionFactory bean,最终注入到SqlSessionTemplate对象中,然后交给其内部的SqlSessionInterceptor来创建新的SqlSession对象。工厂对象的具体创建过程比较复杂,先知道是DefaultSqlSessionFactory类型的即可,后续有时间单独写一篇文章来介绍。

下面来看看工厂对象是怎么创建SqlSession对象的!

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSessionFactory.java
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  @Override
  public SqlSession openSession(boolean autoCommit) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
  }

  @Override
  public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }

  @Override
  public SqlSession openSession(TransactionIsolationLevel level) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
    return openSessionFromDataSource(execType, level, false);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
    return openSessionFromDataSource(execType, null, autoCommit);
  }

  @Override
  public SqlSession openSession(Connection connection) {
    return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, Connection connection) {
    return openSessionFromConnection(execType, connection);
  }

  @Override
  public Configuration getConfiguration() {
    return configuration;
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建sqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        // 使用数据库的配置
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won't support transactions

        // 如果报错,则使用自动提交
        autoCommit = true;
      }
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 创建事务对象
      final Transaction tx = transactionFactory.newTransaction(connection);
      // 创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    // 在Spring中,返回的是SpringManagedTransactionFactory,参考SqlSessionFactoryBean构建SqlSessionFactory时的设置
    return environment.getTransactionFactory();
  }

  private void closeTransaction(Transaction tx) {
    if (tx != null) {
      try {
        tx.close();
      } catch (SQLException ignore) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

}

可以看到,这里创建的是DefaultSqlSession类型的对象。绕了一大圈,都是MyBatis对Spring事务机制的适配,MyBatis主要的实现还是在DefaultSqlSession中。

DefaultSqlSession

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java
@Override
public <T> T selectOne(String statement) {
  return this.selectOne(statement, null);
}

@Override
public <T> T selectOne(String statement, Object parameter) {
  // Popular vote was to return null on 0 results and throw exception on too many.
  // 执行查询,复用列表查询逻辑
  List<T> list = this.selectList(statement, parameter);
  if (list.size() == 1) {
    // 返回查询结果
    return list.get(0);
  } else if (list.size() > 1) {
    // 期待查询1条记录,结果返回了多条则抛出异常
    throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
  } else {
    return null;
  }
}
@Override
public <E> List<E> selectList(String statement) {
  return this.selectList(statement, null);
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
}

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 根据mapper方法的ID(mapper接口的全限定名称拼接上方法的名称)来获取映射语句
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 交给执行器进行查询
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

在这里先获取MappedStatement对象,然后交给Executor来执行。那这里有两个问题,

  • 这里的statement变量是什么?MappedStatement又是什么?
  • executor属性是什么时候创建的?

溯源statement变量

该变量是从上面MapperMethod中一路传下来的,即SqlCommandgetName()方法的返回值。SqlCommand是在构建MapperMethod对象时创建的(MapperMethod是在上面介绍过的MapperProxy中创建的)。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/binding/MapperMethod.java
public static class SqlCommand {

  private final String name;
  private final SqlCommandType type;

  public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    final String methodName = method.getName();
    final Class<?> declaringClass = method.getDeclaringClass();
    // 获取方法对应的SQL语句
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
        configuration);
    if (ms == null) { // 没找到语句
      // 如果方法被@Flush注解修饰
      if (method.getAnnotation(Flush.class) != null) {
        name = null;
        // 则设置SQL类型为FLUSH
        type = SqlCommandType.FLUSH;
      } else {
        // 没找都映射的语句,则报错。这个错误在学习和工作中比较常见。
        throw new BindingException("Invalid bound statement (not found): "
            + mapperInterface.getName() + "." + methodName);
      }
    } else {
      name = ms.getId();
      // 设置SQL语句类型
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }
  }

  public String getName() {
    return name;
  }

  public SqlCommandType getType() {
    return type;
  }

  private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
      Class<?> declaringClass, Configuration configuration) {
    // 计算语句的id,可以看到是mapper接口的全限定名称拼接上接口方法名称
    String statementId = mapperInterface.getName() + "." + methodName;
    // 从配置中获取语句
    if (configuration.hasStatement(statementId)) {
      return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
      return null;
    }
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
      if (declaringClass.isAssignableFrom(superInterface)) {
        MappedStatement ms = resolveMappedStatement(superInterface, methodName,
            declaringClass, configuration);
        if (ms != null) {
          return ms;
        }
      }
    }
    return null;
  }
}

resolveMappedStatement方法中,statement变量其实就是mapper接口名称和方法名称的拼接字符串。而SqlCommandDefaultSqlSession中其实都是通过该拼接串来从Configuration对象中获取MappedStatement对象。

MappedStatement可以看作对一个mapper方法对应的SQL语句的封装,在这里不宜过度展开,后面介绍mapper文件解析的时候会提到。

溯源Executor

Executor对象实际上是在DefaultSqlSessionFactory中创建DefaultSqlSession对象时创建的,然后作为构造方法参数传入其中的。

Executor是MyBatis四大核心接口,该类型对象是在Configuration中创建的。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/session/Configuration.java
public Executor newExecutor(Transaction transaction) {
  return newExecutor(transaction, defaultExecutorType);
}

// 创建执行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  Executor executor;
  // 根据执行器类型,创建不同的执行器对象

  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    // 默认情况下使用SIMPLE执行器
    executor = new SimpleExecutor(this, transaction);
  }
  // 如果开启了缓存
  if (cacheEnabled) {
    // 则封装一下执行器
    executor = new CachingExecutor(executor);
  }
  // 应用拦截器
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

默认情况下创建的是SimpleExecutor类型的执行器,如果启用了缓存机制(cacheEnabled属性来控制,默认是true;可以调整为false来禁用二级缓存;),则会创建CachingExecutor来封装一下。

MyBatis中的四大核心接口:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

这四大接口类型的对象都由Configuration对象来创建,并利用MyBatis的插件机制来处理。插件只应该也只能处理这四种类型的对象。

Executor

CachingExecutor

v3.x
query
getBoundSql
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/executor/CachingExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取实际的SQL(处理完动态SQL后),其中可能还包含参数占位符
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 创建缓存key
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  // 执行查询
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  /*
   * 获取二级缓存对象,可见这里与SqlSession无关,具体与什么有关,要看MappedStatement的cache属性的设置逻辑。
   * (实际上这里的二级缓存以namespace维度来隔离)
   * 所以也说二级缓存与sqlSession无关,与namespace才有关。
   */
  Cache cache = ms.getCache();
  if (cache != null) {
    // 如果mapper中的语句设置了flushCache=true(默认值为false),清空二级缓存;实际上该字段配置还会清空一级缓存(见BaseExecutor)。
    flushCacheIfRequired(ms);
    /*
     * 如果当前语句使用缓存,且没有设置结果处理器
     * 如果mapper中的语句配置了useCache=false(默认值是true),则表示关闭二级缓存。
     */
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 未命中缓存
      if (list == null) {
        // 委托给普通执行器查询
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 将查询结果放入二级缓存中
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托给普通执行器查询
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache);
  }
}
java
mybatis-3/src/main/java/org/apache/ibatis/mapping/MappedStatement.java
public BoundSql getBoundSql(Object parameterObject) {
  // 通过SqlSource来创建BoundSql对象,传入实际参数,处理动态SQL
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  // 获取SQL中的#{}占位符对应的参数信息,被封装为ParameterMapping对象
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  /*
   * 什么情况下为空? (如果XML内容中没有#{}占位符,就为empty,倒不会为null,因为有初始化)
   * 这里parameterMap返回的ParameterMapping是空列表,具体参考上面Builder内部类的初始化操作。
   *
   * MyBatis已经不再推荐在sql标签上设置parameterMap属性,实际工作中也基本不会使用该属性
   * 所以这里基本上可以理解为无效操作,可以忽略掉!
   */
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  // check for nested result maps in parameter mappings (issue #30)
  /*
   * 获取设置resultMap
   * 为什么这里resultMap信息封装在ParameterMapping中的?
   * 参考SqlSourceBuilder中的ParameterMappingTokenHandler内部类中的buildParameterMapping方法
   *
   * 另外可以忽略这里,暂时不知道什么场景会在#{}中指定resultMap。
   */
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

CachingExecutor是MyBatis中的二级缓存实现类。Cache对象是从MappedStatement对象中获取的,但实际上是一个namespace下的所有语句共用一个Cache对象。需要mapper文件中配置有<cache />标签二级缓存才会生效,否则MappedStatement中的引用的cache对象是null。 另外,如果语句标签中配置有flushCache属性且设置为true,则在该语句执行前会清空二级缓存,实际上该属性还会清空一级缓存(见下面介绍)。 另外,还需要语句中的useCache属性为true二级缓存才会生效,只不过该属性默认是true,如果需要对语句级别禁用二级缓存,则可以调整为false

总结一下二级缓存的开关:

  • cacheEnabled:主配置,影响全局;
  • <cache />标签,影响namespace;
  • useCache属性,影响语句;

这三个开关影响的粒度依次降低,可以很灵活地实现二级缓存开启和关闭。

对于MappedStatement中的Cache对象,这里做简要介绍,主要是在MyBatis的启动阶段,扫描mapper文件时会创建MappedStatement语句,并设置Cache对象。

v3.x
cacheElement
MapperBuilderAssistant
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java
private void cacheElement(XNode context) {
  if (context != null) {
    // 解析type属性
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    // 解析eviction属性
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    // 解析flushInterval属性
    Long flushInterval = context.getLongAttribute("flushInterval");
    // 解析size属性
    Integer size = context.getIntAttribute("size");
    // 即欸新readOnly属性
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    // 解析blocking属性
    boolean blocking = context.getBooleanAttribute("blocking", false);
    // 获取子节点配置
    Properties props = context.getChildrenAsProperties();
    // 构建Cache对象
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}
java
mybatis-3/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java
private String currentNamespace;
private final String resource;
private Cache currentCache;
private boolean unresolvedCacheRef; // issue #676
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  // 构建Cache对象
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  // 将Cache对象添加到配置中
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}
// 该方法其实就是在创建一个MappedStatement对象,然后把各种属性设置在其中
public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {

  if (unresolvedCacheRef) {
    throw new IncompleteElementException("Cache-ref not yet resolved");
  }

  id = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  // 创建用于构建statement的builder对象,其实这里往builder上设置的属性都会设置到MappedStatement对象中。
  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource)
      .fetchSize(fetchSize)
      .timeout(timeout)
      .statementType(statementType)
      .keyGenerator(keyGenerator)
      .keyProperty(keyProperty)
      .keyColumn(keyColumn)
      .databaseId(databaseId)
      .lang(lang)
      .resultOrdered(resultOrdered)
      .resultSets(resultSets)
      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
      .resultSetType(resultSetType)
      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);

  // 获取语句的参数映射
  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }

  // 执行构建,这里其实仅是返回内部封装的MappedStatement对象
  MappedStatement statement = statementBuilder.build();
  /*
   * 添加statement
   * 其实在启动时的解析阶段,就是为每个语句创建MappedStatement对象;
   * 在运行时,Executor从Configuration中获取MappedStatement对象,然后通过其内部的SqlSource对象来创建BoundSql对象。
   */
  configuration.addMappedStatement(statement);
  return statement;
}

最后要注意,二级缓存的数据不是缓存到Cache对象中的,而是存储在一个大Map全局对象中的,Cache只不过是作为namespace级别的key,而在上面query方法中创建的CacheKey对象则相当于是语句级别的key。MyBatis的二级缓存就是这样一个两层缓存结构。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/cache/TransactionalCacheManager.java
public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

}

如果二级缓存未命中,或者没有开启二级缓存,则通过delete对象来执行查询(委托模式)。

BaseExecutor

这是一个抽象类,是SimpleExecutor等执行器的父类,其内部实现了CachingExecutor委托调用的query方法,该方法主要是实现了一级缓存(也被成为local缓存,即本地缓存)。各子类继承了该方法,只需实现缓存未被命中时的底层查询过程。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/executor/BaseExecutor.java

private static final Log log = LogFactory.getLog(BaseExecutor.class);

protected Transaction transaction;
protected Executor wrapper;

protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;

protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
  this.transaction = transaction;
  this.deferredLoads = new ConcurrentLinkedQueue<>();
  this.localCache = new PerpetualCache("LocalCache");
  this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
  this.closed = false;
  this.configuration = configuration;
  this.wrapper = this;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  /*
   * 获取BoundSql对象
   * 每次执行都要获取一次BoundSql对象,是因为MyBatis的动态SQL机制,不同参数对应了不同的SQL内容,所以每次执行时才获取。
   * 这里内部是通过MappedStatement中的SqlSource来获取的,这一步是分析MyBatis动态SQL的入口。
   *
   * 如果mapper方法有多个参数,那么这里只有一个Object类型的parameter参数是什么情况?
   *  是因为在上层的MapperMethod中会根据参数进行封装,不同情况会有不同的封装,为了兼容,这里使用Object。
   */
  BoundSql boundSql = ms.getBoundSql(parameter);
  // 创建缓存键
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  // 执行查询
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  /*
   * 清空本地(一级)缓存
   * 如果mapper中的语句配置了flushCache=true(默认值是false),则首次执行查询时清空一级缓存,后续的嵌套执行产生的缓存取决于localCacheScope。
   * 这里的queryStack == 0用于判断当前查询是否没有被嵌套在其他查询中。
   *  如果存在嵌套,是怎么触发嵌套查询的?以及执行流程是怎样的(是怎么执行到这里的)?
   */
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    // 从一级缓存中查询
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) { // 一级缓存中存在记录
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 查询数据库
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    // 处理延迟加载
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    // 清空延迟加载对象
    deferredLoads.clear();
    /*
     * 如果一级缓存的级别是语句,则清空(为什么这里要清空,清空了岂不是缓存就没意义了?)
     * 如果在主配置中将缓存的scope设置为STATEMENT,那么表示禁用一级缓存,
     *   因为上面的queryFromDatabase方法中会将查询结果设置到一级缓存中,所以这里要清空。
     *   其实设置一级缓存时就应该判断是否开启了一级缓存,没开启就不用设置,避免无效操作。
     */
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
@Override
public void clearLocalCache() {
  if (!closed) {
    localCache.clear();
    localOutputParameterCache.clear();
  }
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // 为什么要在一级缓存中放入占位对象
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    // 执行数据库查询,这是一个模板方法,参考默认的执行器SimpleExecutor中的实现
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    // 移除上面放入的占位对象
    localCache.removeObject(key);
  }
  // 将查询结果设置到一级缓存中
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
    throws SQLException;

在上面介绍溯源Executor时,提到了是在SqlSessionFactory中创建SqlSession对象时创建的Executor对象。所以验证了“MyBatis中的一级缓存与SqlSession有关”的说法。 在query方法中可以看到,如果主配置中的localCacheScope属性是STATEMENT,那么会禁用一级缓存,即每次执行语句时,都会清空localCache。另外,正如上面介绍二级缓存所说到的,如果语句配置了flushCache属性为true,那么也会清空localCache

一级缓存PerpetualCache内部也有一个Map属性,用来存储数据。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/cache/impl/PerpetualCache.java
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

query方法中实现了一级缓存的访问,在queryFromDatabase方法中实现了一级缓存的写入。如果一级缓存未被命中,那么则会调用doQuery方法,这是一个模板方法,由各子类来实现。

SimpleExecutor

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/executor/SimpleExecutor.java
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 获取配置对象
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 通过Statement来处理SQL语句
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 执行查询
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  // 获取JDBC连接
  Connection connection = getConnection(statementLog);
  // 处理语句
  stmt = handler.prepare(connection, transaction.getTimeout());
  // 为语句设置参数
  handler.parameterize(stmt);
  return stmt;
}

该方法主要执行三个操作:

  • 创建StatementHandler(MyBatis四大核心接口之一)对象;
  • 创建Statement对象;
  • Statement对象设置参数;
  • 执行语句;

创建StatementHandler对象

v3.x
newStatementHandler
RoutingStatementHandler
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/session/Configuration.java
// 创建语句处理器
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 对语句处理器应用插件
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/RoutingStatementHandler.java
public class RoutingStatementHandler implements StatementHandler {

  /*
   * 所有的操作都委托给该对象来进行,目的是通过一个对象来封装三种不同类型的对象。
   */
  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    // 根据语句的类型不同,创建不同的委托对象
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);
  }

  @Override
  public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
  }

  @Override
  public void batch(Statement statement) throws SQLException {
    delegate.batch(statement);
  }

  @Override
  public int update(Statement statement) throws SQLException {
    return delegate.update(statement);
  }

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    return delegate.query(statement, resultHandler);
  }

  @Override
  public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
    return delegate.queryCursor(statement);
  }

  @Override
  public BoundSql getBoundSql() {
    return delegate.getBoundSql();
  }

  @Override
  public ParameterHandler getParameterHandler() {
    return delegate.getParameterHandler();
  }
}

这里创建的其实是RoutingStatementHandler类型的对象,内部封装了实际的StatementHandler对象,这样设计的目的是封装三种不同类型的StatementHandler,本文只会分析PreparedStatementHandler,这也是大多数情况。

创建Statement对象

先是创建了JDBC中的Connection对象(终于接触到JDBC了),然后交给StatementHandler用来创建Statement对象。 三种StatementHandler的prepare实现都在抽象父类BaseStatementHandler中。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/BaseStatementHandler.java
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    // 创建和初始化statement对象
    statement = instantiateStatement(connection);
    // 设置语句超时时间
    setStatementTimeout(statement, transactionTimeout);
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
  }
}
protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

核心步骤在instantiateStatement方法中,这是个模板方法,由子类实现。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java
/*
 * 都是通过JDBC的Connection的prepareStatement方法来创建java.sql.PreparedStatement对象
 */
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
  // 获取SQL字符串,参数位置由占位符?代替,后续步骤会调用下面的parameterize方法来用实际参数替换占位符
  String sql = boundSql.getSql();
  if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
    String[] keyColumnNames = mappedStatement.getKeyColumns();
    if (keyColumnNames == null) {
      return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
    } else {
      return connection.prepareStatement(sql, keyColumnNames);
    }
  } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    return connection.prepareStatement(sql);
  } else {
    return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  }
}

可以发现,这里终究还是通过Conection对象创建的Statement对象。

Statement对象设置参数

v3.x
parameterize
BaseStatementHandler
<
>
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java
@Override
public void parameterize(Statement statement) throws SQLException {
  // 通过参数处理器来设置参数
  parameterHandler.setParameters((PreparedStatement) statement);
}
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/BaseStatementHandler.java
protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;

protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final RowBounds rowBounds;

protected BoundSql boundSql;
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  this.configuration = mappedStatement.getConfiguration();
  this.executor = executor;
  this.mappedStatement = mappedStatement;
  this.rowBounds = rowBounds;

  this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  this.objectFactory = configuration.getObjectFactory();

  if (boundSql == null) { // issue #435, get the key before calculating the statement
    generateKeys(parameterObject);
    boundSql = mappedStatement.getBoundSql(parameterObject);
  }

  this.boundSql = boundSql;

  // 创建参数处理器
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  // 创建结果集处理器
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

实际的参数设置操作是交给ParameterHandler来进行的。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java
@Override
public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // 获取SQL中的参数占位符
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      /*
       * 这个mode很少用,一般与存储过程有关,当然没用时getMode返回null,所以这里也是满足条件的
       */
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 获取TypeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          // 通过typeHandler来设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

这里会替换PreparedStatement中的参数占位符,这里涉及到了ParameterMappingTypeHandler,这两个对象是MyBatis中SQL解析阶段会涉及到的两种类型,这里暂时不用关注是怎么来的。接着看TypeHandler内部的参数设置。

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/type/BaseTypeHandler.java
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
  if (parameter == null) {
    if (jdbcType == null) {
      throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
    }
    try {
      ps.setNull(i, jdbcType.TYPE_CODE);
    } catch (SQLException e) {
      throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
            + "Cause: " + e, e);
    }
  } else {
    try {
      setNonNullParameter(ps, i, parameter, jdbcType);
    } catch (Exception e) {
      throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different configuration property. "
            + "Cause: " + e, e);
    }
  }
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

万变不离其宗,这里仍然是调用ParameterMapping的方法来设置的参数。哪怕是子类实现的setNonNullParameter方法中,仍然如此。

执行语句

v3.x
java
mybatis-3/src/main/java/org/apache/ibatis/executor/statement/PreparedStatementHandler.java
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  // 执行查询,进入JDBC的范畴了
  ps.execute();
  // 使用ResultSetHandler来处理结果
  return resultSetHandler.handleResultSets(ps);
}

终于,执行的主体从这里开始从MyBatis进入底层JDBC。 JDBC执行结束后,MyBatis利用ResultSetHandler来处理结果集,这部分也比较复杂,后续单独开一篇文章来介绍。

总结

本文介绍了MyBatis的mapper方法的执行主体过程,调用链整体还是比较长的,所以本文分为三个部分来介绍。在统一拦截层介绍了mapper方法实参名称的处理;在SqlSession层介绍了MyBatis与Spring事务的整合;在Executor层介绍了MyBatis一二级缓存的实现原理。后续的动态SQL处理和结果集映射分析都会基于本文。