🌑

Shawn Fux

Mybatis是如何执行一条SQL语句的

从调用 openSession 获得一个 SqlSession 开始分析,再到从调用 getMapper 方法获得一个接口的实现,调用接口的方法执行 SQL 获得结果。这其中 Mybatis 是都做了些什么?是如何运作的?

创建SqlSession

SqlSession sqlSession = sqlSessionFactory.openSession();

在上一篇文章了已经分析了我们如何获得构建一个 sqlSessionFactory ,现在我们就可以通过 sqlSessionFactory 获得一个 SqlSession ,主要是向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给 Executor ,我们先看一下 openSession 方法做了些什么:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 获取当前的环境配置对象
    final Environment environment = configuration.getEnvironment();
    // 获取环境配置对象的 TransactionFactory,如果不存在创建默认的 ManagedTransactionFactory
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 创建一个事务
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 创建执行器对象
    final Executor executor = configuration.newExecutor(tx, execType);
    // 将 Executor 包装到 DefaultSqlSession 返回
    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();
  }
}

首先获取通过配置类 configuration 获取当前执行的环境变量对象,而在 Environment 对象就保存了 DataSource 数据源和事务工厂 TransactionFactory ,然后通过事务工厂创建了一个事务。

newExecutor

接下来看一下 newExecutor 方法,将我们当前创建的事务和参数 ExecutorType 传入来创建一个指定类型的 Executor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  // 确定要使用的的 Executor 类型
  // 先判断参数 ExecutorType 是否为空,如果为空使用成员变量的 defaultExecutorType
  executorType = executorType == null ? defaultExecutorType : executorType;
  // 如果成员变量的 defaultExecutorType 为空,使用 ExecutorType.SIMPLE
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    // 批处理执行器
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    // 可重用执行器
    executor = new ReuseExecutor(this, transaction);
  } else {
    // 默认的每次开启一个Statement对象,用完立刻关闭Statement对象
    executor = new SimpleExecutor(this, transaction);
  }
  // 如果二级缓存开关开启的话,是使用CachingExecutor装饰BaseExecutor的子类
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 为 Executor 进行织入拦截器,生成代理对象
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

首先会判断你是否有传 ExecutorType ,没有的话就会去推断到底应该使用那个类型去创建对应的 Executor ,而 Mybatis 提供了三种 Executor 类型:

  • BatchExecutor:批量执行器,默认情况是每次执行一条sql,MyBatis都会发送一条sql。而批量执行器的操作是,每次执行一条sql,不会立马发送到数据库,而是批量一次性发送多条sql。
  • ReuseExecutor:可重用的执行器,重用的是Statement对象,第一次执行一条sql,会将这条sql的Statement对象缓存在key-value结构的map缓存中。下一次执行,就可以从缓存中取出Statement对象,减少了重复编译的次数,从而提高了性能。每个SqlSession对象都有一个Executor对象,因此这个缓存是SqlSession级别的,所以当SqlSession销毁时,缓存也会销毁。
  • SimpleExecutor:简单类型的执行器,也是默认的执行器,每次执行update或者select操作,都会创建一个Statement对象,执行结束后关闭Statement对象。

在确定了要创建那种类型的 ExecutorType 后,会去判断 cacheEnabled 这个值是否为 true,即判断你是否开启了二级缓存,如果开启了二级缓存会使用 CachingExecutor 对创建的 Executor 作一层包装,这是装饰器模式的设计体现。

Interceptor插件

创建完 Executor 对象以后,会判断当前是否有 Executor 类型匹配的拦截器插件,如果有的话会为其织入拦截器插件产生代理对象。首先了解一下如何定义一个插件:

@Intercepts({@Signature(
        type= Executor.class,
        method = "query",
        args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
public class MyExecutorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyExecutorInterceptor1");
        return invocation.proceed();
    }
}

首先你的插件要实现 Interceptor 接口,然后需要使用 @Intercepts 注解指定你要切面的方法,而 @Signature 注解的 type,就是指定你要拦截的类型接口,由于是基于 JDK 的动态代理,所以只能拦截接口,另外值的注意的事 Mybatis 只提供了四个接口能够拦截有效,分别是 ExecutorStatementHandlerParameterHandlerResultSetHandler ,如果你指定其它的接口是不会有任何效果的,因为在源码就已经硬编码好了,只有在创建这四个接口的实现类,才会去生成代理对象,而 method 属性就是指定你要拦截的接口方法,但是方法是可以重载的,所以光有方法名不能定位唯一,还需要使用 args 方法参数类型签名。

最后你定义了一个拦截器插件,还需要注册配置,如果你是使用 xml 配置文件的话:

<plugins>
    <plugin interceptor="com.demo.interceptor.MyExecutorInterceptor"/>
</plugins>

经过上面的两个步骤以后,我们已经定义一个拦截器插件,接下来看 Mybatis 是如何生成代理对象的:

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

首先会遍历所有的 Interceptor 调用 plugin 方法去匹配:

default Object plugin(Object target) {
  return Plugin.wrap(target, this);
}

plugin 方法是 Interceptor 接口的默认实现方法,然后又调用了 Plugin 的静态方法 wrap ,将当前拦截器对象和当前被拦截的对象,在本例子中是 Executor 对象:

public static Object wrap(Object target, Interceptor interceptor) {
  // 解析当前拦截器需要拦截那个类的那个方法
  // Key 为被拦截的类型,value 为被拦截的方法
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 获取被代理对象的类型
  Class<?> type = target.getClass();
  // 获取被代理对象的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 使用 JDK 动态代理创建对象
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

首先调用 getSignatureMap 方法去解析当前拦截器对象要拦截那些类和方法,就是解析 @Signature 注解,最后再通过 getAllInterfaces 方法判断当前拦截器拦截的接口,有没有能和 target 匹配的接口。

DefaultSqlSession

最后就是创建 new DefaultSqlSession(configuration, executor, autoCommit); 了一个默认的 DefaultSqlSession,将当前的配置类对象,以及 Executor 和是否自动提交布尔值传递进去了。

getMapper

通过上面的步骤。我们获得了一个 SqlSession 对象,接下来就可以通过 getMapper 方法获取一个接口的实现类:

// DefaultSqlSession 类的
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}

内部实际是调用了 configuration 配置类的 getMapper 方法:

// Configuration 类的
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

接着又调用了 configuration 配置类成员变量 mapperRegistry 方法,MapperRegistry 类主要职责就是注册我们的 Mapper 接口和获取 Mapper 接口的实现类

// MapperRegistry 类的
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 根据类型去获取对应的 MapperProxyFactory
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 基于 JDK 动态代理创建代理对象返回
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

首先以当前 Mapper 接口类型为 Key,去 knownMappers Map 集合去找对应的 MapperProxyFactory ,而 MapperProxyFactory 就是用来创建代理对象的,所以重点看一下 newInstance 方法,是如何创建代理对象的:

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

首先将当前的 SqlSession 作为 MapperProxy 构造函数的参数传递进去,而 MapperProxy 实现了 InvocationHandler 接口,所以我们对 Mapper 接口的方法调用最终都会调用到 MapperProxy 对象的 invoke 方法。

public class MapperProxy<T> implements InvocationHandler, Serializable {
  @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 {
        // 判断缓存是否存在该方法对应的 PlainMethodInvoker
        // 如果没有创建返回
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

最后就是使用 JDK 的动态代理创建代理对象并返回给调用者了:

protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

Mapper接口方法入口点

在上一步我们通过 SqlSessiongetMapper 获得了 Mapper 接口的代理对象,然后又知道了,所有 Mapper 接口的方法最终都会被代理拦截到 MapperProxy 对象的 invoke 方法,接下来就分析 invoke 方法都做了些什么?

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 {
      // 判断缓存是否存在该方法对应的 PlainMethodInvoker
      // 如果没有创建返回
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

首先判断当前被代理的方法,是不是来自 Object 类,比方说 hashCode 这些方法都是会忽略的,不会把它当做一个 SQL 接口方法,当确认了不是来自 Object 类后,会把当前被代理的方法传递给 cachedInvoker 方法:

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 {
        // 不是接口默认方法,直接创建返回
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

它这里用了会先判断 methodCache 缓存中是否有解析好的该方法对应的 MapperMethodInvoker 对象,如果没有才会调用第三个参数 lambda 表达式去执行创建并放入 methodCache 缓存中,而在这个 lambda 表达式主要有两个分支,先判断你是不是接口 default 方法,如果不是直接创建一个 PlainMethodInvoker 对象,而在 PlainMethodInvoker 的构造函数参数还创建了 MapperMethod 对象,我们先分析怎么创建 MapperMethod 对象的:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 确定 SQL 操作类型
    this.command = new SqlCommand(config, mapperInterface, method);
    // 确定方法的形参位置,以及返回值类型判断
    this.method = new MethodSignature(config, mapperInterface, method);
  }
 }

创建SqlCommand

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();
    // 解析该方法对应的 MappedStatement,即 SQL 片段
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
        configuration);
    if (ms == null) {
      // 如果没找到对应的 SQL 片段,那么检查是否有 @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();
      type = ms.getSqlCommandType();
      if (type == SqlCommandType.UNKNOWN) {
        throw new BindingException("Unknown execution method for: " + name);
      }
    }
  }
 }

SqlCommand 主要封装了你当前执行的 SQL 类型和你所关联的 MappedStatement ID,一般来说这会 ID 就是接口全限定类名加方法名,这就导致了我们在同一个Mapper 中不能出现重载的接口方法,每个 MappedStatement 对应了我们自定义 Mapper 接口中的一个方法,它保存了我们编写的SQL语句、参数结构、返回值结构、Mybatis 对它的处理方式的配置等细节要素,是对一个SQL 命令是什么、执行方式的完整定义。

创建MethodSignature

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();
  }
  // 返回值是否为 void
  this.returnsVoid = void.class.equals(this.returnType);
  // 返回值是否为数组或集合
  this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
  // 是否游标查询
  this.returnsCursor = Cursor.class.equals(this.returnType);
  // 是否为 Optional 封装
  this.returnsOptional = Optional.class.equals(this.returnType);
  // 检查是否有 @MapKey
  this.mapKey = getMapKey(method);
  // 检查返回结果是否为 Map 集合
  this.returnsMap = this.mapKey != null;
  // 确定 RowBounds 在方法参数的索引位置
  this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
  // 确定 ResultHandler 在方法参数的索引位置
  this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
  // 解析方法的索引以及对应形参名字
  this.paramNameResolver = new ParamNameResolver(configuration, method);
}

MethodSignature 主要封装了你当前方法的一些参数解析信息和返回值信息,我们主要看一下 new ParamNameResolver 对象这一句,这个和我们在 xml 使用接口的参数息息相关,比方说我有如下一个接口方法签名:

List<User> select(@Param("myName") String name, Integer age, @Param("myMileNo") String mobileNo);
// 我的调用方法参数是这样的
List<User> userList = userMapper.select("李四",22,"188110");

那么通过创建 ParamNameResolver 对象会在构造函数里解析我当前的方法形参名
ParamNameResolver

names

最终会得到一个这样的形参名映射到实参的 Map 集合,优先使用 @Param 注解的 value 值作为形参,那么你可能会想不是还支持 param1 param2 … 这样的映射关系吗,的确是支持的,不过不是在这一步处理的,后面的分析会看到的。

最后就是创建 PlainMethodInvoker 对象了

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

PlainMethodInvoker 构造方法并没有做什么逻辑处理,只是简单的将参数 MapperMethod 对象记录到了直接的成员变量上面。

cachedInvoker 方法会将创建好后的 PlainMethodInvoker 对象放入缓存缓存,并返回给调用者,当我们得到了 PlainMethodInvoker 对象以后,然后就调用了它的 invoke 方法:

// PlainMethodInvoker 类的
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args);
}

execute

invoke 方法又调用了 MapperMethod 对象的 execute 方法:

// MapperMethod 类的
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 根据 SQL 操作的类型不同,调用不同的方法
  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()) {
        // 如果方法返回值为 void,并且方法参数有 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()) {
        // 如果方法返回值为游标
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

上面 execute 方法就会根据你当前的 SQL 类型以及返回值的差异,从而决定调用 SqlSession 的那一个方法去执行 SQL 来获取结果。

SqlSession方法执行

虽然上面的步骤分支很多,但是最终都会委托调用 SqlSession 的方法去干活,这里我就以 executeForMany 这个分支为例子,这个分支主要是用来处理,返回结果为集合或数组的,也是我们平常用的比较多的。

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    // 对参数添加映射关系,param1 => 实参
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      // 如果方法参数有 RowBounds,通过预先找到的索引位置,取出RowBounds
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
  
    // 判断方法的返回值是否实际结果的返回值是否兼容一致
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      // 如果方法的返回值数组
      if (method.getReturnType().isArray()) {
        // 那么将集合转换为数组返回
        return convertToArray(result);
      } else {
        // 如果不是数组,将实际结果类型转换为方法返回值期望类型
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

首先看一下 convertArgsToSqlCommandParam 这个方法就会为我们添加新的形参名到实参的映射关系,就是 param1,param2…. 这样的映射关系。然后判断你的参数是否有 RowBounds ,这个参数是 Mybatis 内置的一个设置内存分页的参数,一般来说比较少使用,最后就是会对执行的结果作一些处理判断,判断你的执行结果返回类型与方法定义的返回值是否能够兼容。

selectList

result = sqlSession.selectList(command.getName(), param);

如果你的参数没有 RowBounds 的话,那么就会执行到上面这个语句调用 SqlSessionselectList 方法,这里将当前的 command 对象的 name 属性和方法参数映射对象传递进去了,而 command 对象的 name 其实就是 MappedStatement ID。

// DefaultSqlSession 类的
@Override
public <E> List<E> selectList(String statement, Object parameter) {
  // 调用了重载带 RowBounds 参数的 selectList 方法
  // RowBounds.DEFAULT 就是传了个 null
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
// DefaultSqlSession 类的
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  // 调用了重载带 ResultHandler 参数的 selectList 方法
  // Executor.NO_RESULT_HANDLER 就是传了个 null
  return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
}
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 通常来说是接口名加方法名组成 statement 的Id
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 这里会执行 Executor 的拦截器插件方法,如果有设置对应的插件的话
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

首先通过参数 statement 去配置类 configuration 获取对应的 MappedStatement ,然后调用的 Executorquery 方法去执行查询操作,如果你有设置拦截 Executor 接口的对应的方法拦截器插件的话,会先执行拦截器的插件方法。

Executor的query方法

如果你开启了二级缓存的话,会先执行 CachingExecutorquery 方法,如果没有二级缓存的话,就直接执行 BaseExecutorquery 方法。我这里我假设我开启了二级缓存:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 根据当前参数解析动态 SQL,为静态 SQL
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 生成 SQL 语句的缓存 Key
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  // 二级缓存的实现
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

解析BoundSql

首先会将当前的动态 SQL 解析成静态的 SQL,因为对于动态 SQL 来说,有些语句片段需要在明确参数的条件下才能得出。

SqlNode

在 xml 文件定义的 SQL 语句会被解析成一个个 SqlNode 节点对象,而每个节点对象都会有有一个方法,根据当前 context 参数决定自己的节点需要添加什么样的 SQL 片段到 context。

public interface SqlNode {
  boolean apply(DynamicContext context);
}
public class DynamicContext {
	  /**
   * 里面包含了当前的方法实参
   */
  private final ContextMap bindings;
  /**
   * SQL 语句拼接最终字符串
   */
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
}

DynamicContext 有两个比较重要的成员变量,比方说我有一个如下的动态 SQL 语句:

<select id="select1" resultType="user">
    SELECT * FROM user
        <if test="myName != null and myName != ''">
            WHERE name like #{myName}
        </if>
</select>

那么 <if> 标签就会对应 IfSqlNode 节点对象,Mybatis 会结合当前的参数与 if 标签的 test 条件使用 ognl 表达式,判断条件是否成立,如果成立的话,会把 if 标签的 WHERE name like #{myName} 内容添加到 DynamicContextsqlBuilder 属性上最终形成一个完整的 SQL 语句。这里 Mybatis 使用了组合设计模式。

生成缓存Kye

解析完动态 SQL 以后,就会根据当前的 SQL 语句参数以及 MappedStatement ID,还有环境变量对象的ID生成一个缓存 Key,用来后续作为一二级缓存的 Key。

二级缓存的实现

二级缓存是由 CachingExecutor 类实现的

// CachingExecutor 类的
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  // 判断是否有开启二级缓存
  if (cache != null) {
    flushCacheIfRequired(ms);
    // 如果使用了 resultHandler,二级缓存也不会生效
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 先从事务缓存中获取
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 事务缓存中不存在,执行SQL获取结果
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 放入事务缓存
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 执行SQL获取结果
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

Cache 装饰器模式

cache

可以看到 Cache 使用了装饰器模式去组合,最底层的 PerpetualCache 是实际负责存储我们的缓存数据的,而第二层的 FifoCache 是一种缓存清楚策略,Mybatis 除了提供 FIFO 先进先出:按对象进入缓存的顺序来移除它们的策略,还提供了 LRU 最近最少使用,SOFT 软引用:基于垃圾回收器状态和软引用规则移除对象,WEAK 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。这种模式实现的好处就是职责单一,方便替换。

事务缓存

在二级缓存中还引入了一层事务缓存的概念,比方说 SQL 执行成功,它会先放入事务缓存,等你的事务提交后才会把它放入真正的缓存中,如果你回滚了,那么只需要清空事务缓存内存即可。

一级缓存的实现

当二级缓存没找到的话,就会调用 BaseExecutorquery 方法,而一级缓存是由 BaseExecutor 实现的:

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.");
  }
  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();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

BaseExecutorquery 方法里,就没有判断是否开启一级缓存的逻辑了,而是直接使用 localCache 尝试从缓存获取一个结果集,没有找到的话就调用 queryFromDatabase 方法从数据库获取结果:

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 {
    // 调用子类的doQuery方法执行查询获取结果
    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;
}

在前面 newExecutor 方法的时候,我们会根据 ExecutorType 从而创建不同的 Executor ,而这些不同实现的 Executor 其实就对应了不同的 JDBC Statement

创建数据库连接执行SQL

这里以 SimpleExecutor 为例进行分析

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对象
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 执行SQL获取返回值
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

创建StatementHandler

首先调用 configuration 配置类的 newStatementHandler 方法去创建一个 StatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // 根据 MappedStatement 类型不同创建不同的 StatementHandler
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 插件织入,生成代理对象
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

newStatementHandler 方法内部又创建了一个 RoutingStatementHandler 对象,由它的内部决定去创建一个什么样的 StatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
	// 根据当前的 MappedStatement 类型不同
  // 创建不同的 StatementHandler
  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());
  }

}

创建数据库连接

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  // 获取数据库连接
  Connection connection = getConnection(statementLog);
  // 创建JDBC 的 Statement
  stmt = handler.prepare(connection, transaction.getTimeout());
  // 执行 ParameterHandler 的 setParameters 方法
  // 将参数绑定到 JDBC 的 Statement
  // 如果有插件会在这里执行
  handler.parameterize(stmt);
  return stmt;
}

执行SQL语句

// PreparedStatementHandler  为例
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // JDBC 的 API,执行SQL语句
    ps.execute();
    // 处理结果返回值
    return resultSetHandler.handleResultSets(ps);
}

处理返回值

, — Sep 25, 2022