mybatis源码解析一(整体了解) 精华

我要换个网名。
发布于 2022-6-1 17:35
浏览
4收藏

观看的前提:你使用过MyBatis。

一、MyBatis是干什么的?

抛开自身的定义不说。其本质就是操作数据库的,因为是在java写的,所以逃不脱底层使用java的JDBC,通过实现相关底层接口调用mysql oracle等相关数据库驱动,从而实现数据库的各种功能,我们研究源码就是看本质。其关系如下图:
mybatis源码解析一(整体了解)-鸿蒙开发者社区

二、MyBatis 如何操作数据库?

2.1JDBC执行流程

mybatis源码解析一(整体了解)-鸿蒙开发者社区

2.2mybatis 执行流程

mybatis源码解析一(整体了解)-鸿蒙开发者社区
其执行过程堆栈如下图:
mybatis源码解析一(整体了解)-鸿蒙开发者社区

• 方法代理:与MyBatis交互的门面,存在的目的是为了方便调用,本身不会影响执行逻辑。(也就是说可以直接去掉,只是调用会麻烦些)
• 会话:与MyBatis交互的门面,所有对数据库操作必须经过它,但它不会真正去执行业务逻辑,而是交给Execute。另外他不是线程安全的所以不能跨线程调用。
• 执行器:真正执行业务逻辑的组件,其具体职能包括与JDBC交互,缓存管理、事物管理等。

2.3举例说明

以上过程可以类比成你去饭店点餐。
1、由服务员(SqlSession 门面设计模式)为你下单。
2、交给后厨(Execute 责任链、模板方法设计模式 )制作。
3、后厨他们分工合作职责分明,有做中餐的(SimpleExecutor)、有洗菜的(BaseExecutor 共性处理)。
4、还有负责打包整理端盘的(ResultHandler 数据库行数据转换成java对象处理器)。
5、最后在由服务员交到你手上。服务员不能同时为两个顾客点单(SqlSession 不能同时为两个线程共用)。
这个方法代理MapperMethod 怎么表示呢?这就是叫外卖呀!你的点餐过程被外卖公司代理了,但最终还是要由服务员接单,后厨制作。(屏蔽底层实现)

当然这里还有很多细节(配置解析、缓存实现、线程安全、sql解析、参数解析、结果解析等),后续会结合源码去一一讲解。而你现在需要记住的是总体的执行过程即:方法代理–>会话–>执行器–>JDBC

2.4mybatis涉及到类总体标注

类名                   说明
MapperProxy     用于实现动态代理,是InvocationHandler接口的实现类。
MapperMethod    主要作用是将我们定义的接口方法转换成MappedStatement对象。
DefaultSqlSession  默认会话。

CachingExecutor    二级缓存执行器(这里没有用到)。
BaseExecutor       抽像类,基础执行器,包括一级缓存逻辑在此实现。
SimpleExecutor     可以理解成默认执行器。

JdbcTransaction     事物管理器,会话当中的连接由它负责。
PooledDataSource    myBatis自带的默认连接池数据源。
UnpooledDataSource  用于一次性获取连接的数据源。

BaseStatementHandler      StatementHandler基础类。
PreparedStatementHandler  Sql预处理执行器。
DefaultParameterHandler   默认预处理器实现。
StatementHandler          SQL执行处理器。
RoutingStatementHandler   用于根据 MappedStatement 的执行类型确定使用哪种处理器:如STATEMENT(单次执行)、PREPARED(预处理)、 CALLABLE(存储过程)。

BaseTypeHandler         java类型与JDBC类型映射处理基础类。
IntegerTypeHandler      Integer与JDBC类型映射处理。
PreparedStatementLogger 用于记录PreparedStatement对像方法调用日志。
ConnectionLogger        用于记录Connection对像的方法调用日志。

三、mybatis扩展插件原理

在编写 mybatis 插件的时候,首先要实现 Interceptor 接口,如果是使用spring体系 可以通过@Intercepts方式实现下面会有单独的介绍、也可以通过在配置文件 mybatis-conf.xml 中添加插件下列配置的方式实现

<configuration>
  <plugins>
    <plugin interceptor="***.interceptor1"/>
    <plugin interceptor="***.interceptor2"/>
  </plugins>
</configuration>

这里需要注意的是,添加的插件是有顺序的,因为在解析的时候是依次放入 ArrayList 里面,而调用的时候其顺序为:2 > 1 > target > 1 > 2;(插件的顺序可能会影响执行的流程)更加细致的讲解可以参考 ​​QueryInterceptor 规范​​;

然后当插件初始化完成之后,添加插件的流程如下:
mybatis源码解析一(整体了解)-鸿蒙开发者社区
首先要注意的是,mybatis 插件的拦截目标有四个,Executor、StatementHandler、ParameterHandler、ResultSetHandler:

public ParameterHandler (MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

public ResultSetHandler (Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
    ResultHandler resultHandler, BoundSql boundSql) {
  ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  return resultSetHandler;
}

public StatementHandler (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;
}

public Executor (Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  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 {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象; 其责任链的添加过程如下:

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

最终动态代理生成和调用的过程都在 Plugin 类中:

public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取签名Map
  Class<?> type = target.getClass(); // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  // 获取目标接口
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(  // 生成代理
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

这里所说的签名是指在编写插件的时候,指定的目标接口和方法,例如:

@Intercepts({
  @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    ...
  }
}

这里就指定了拦截 Executor 的具有相应方法的 update、query 方法;注解的代码很简单,大家可以自行查看;然后通过 getSignatureMap 方法反射取出对应的 Method 对象,在通过 getAllInterfaces 方法判断,目标对象是否有对应的方法,有就生成代理对象,没有就直接反对目标对象;

在调用的时候:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());  // 取出拦截的目标方法
    if (methods != null && methods.contains(method)) { // 判断这个调用的方法是否在拦截范围内
      return interceptor.intercept(new Invocation(target, method, args)); // 在目标范围内就拦截
    }
    return method.invoke(target, args); // 不在目标范围内就直接调用方法本身
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

3.1、PageHelper 拦截器实现分析

mybatis 插件我们平时使用最多的就是分页插件了,这里以 PageHelper 为例,其使用方法可以查看相应的文档 如何使用分页插件,因为官方文档讲解的很详细了,我这里就简单补充分页插件需要做哪几件事情;

使用:

PageHelper.startPage(1, 2);
List<User> list = userMapper1.getAll();

PageHelper 还有很多中使用方式,这是最常用的一种,他其实就是在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页;(PageHelper 使用的其他方法,也是围绕 Page 对象的设置进行的)

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  Page<E> page = new Page<E>(pageNum, pageSize, count);
  page.setReasonable(reasonable);
  page.setPageSizeZero(pageSizeZero);
  //当已经执行过orderBy的时候
  Page<E> oldPage = getLocalPage();
  if (oldPage != null && oldPage.isOrderByOnly()) {
    page.setOrderBy(oldPage.getOrderBy());
  }
  setLocalPage(page);
  return page;
}
@Intercepts({
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class PageInterceptor implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    try {
      Object[] args = invocation.getArgs();
      MappedStatement ms = (MappedStatement) args[0];
      Object parameter = args[1];
      RowBounds rowBounds = (RowBounds) args[2];
      ResultHandler resultHandler = (ResultHandler) args[3];
      Executor executor = (Executor) invocation.getTarget();
      CacheKey cacheKey;
      BoundSql boundSql;
      //由于逻辑关系,只会进入一次
      if (args.length == 4) {
        //4 个参数时
        boundSql = ms.getBoundSql(parameter);
        cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
      } else {
        //6 个参数时
        cacheKey = (CacheKey) args[4];
        boundSql = (BoundSql) args[5];
      }
      checkDialectExists();

      List resultList;
      //调用方法判断是否需要进行分页,如果不需要,直接返回结果
      if (!dialect.skip(ms, parameter, rowBounds)) {
        //判断是否需要进行 count 查询
        if (dialect.beforeCount(ms, parameter, rowBounds)) {
          //查询总数
          Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
          //处理查询总数,返回 true 时继续分页查询,false 时直接返回
          if (!dialect.afterCount(count, parameter, rowBounds)) {
            //当查询总数为 0 时,直接返回空的结果
            return dialect.afterPage(new ArrayList(), parameter, rowBounds);
          }
        }
        resultList = ExecutorUtil.pageQuery(dialect, executor,
            ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
      } else {
        //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
        resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
      }
      return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
      if(dialect != null){
        dialect.afterAll();
      }
    }
  }
}
  • 首先可以看到拦截的是 Executor 的两个 query 方法(这里的两个方法具体拦截到哪一个受插件顺序影响,最终影响到 cacheKey 和 boundSql 的初始化);
  • 然后使用 checkDialectExists 判断是否支持对应的数据库;
  • 在分页之前需要查询总数,这里会生成相应的 sql 语句以及对应的 MappedStatement 对象,并缓存;
  • 然后拼接分页查询语句,并生成相应的 MappedStatement 对象,同时缓存;
  • 最后查询,查询完成后使用 dialect.afterPage 移除 Page对象

四、mybatis执行器Executro

mybatis源码解析一(整体了解)-鸿蒙开发者社区

4.1创建DefaultSqlSession

通过会话工厂 创建 DefaultSqlSession
DefaultSqlSessionFactory.openSession().openSessionFromDataSource()
方法的入参通过mybatis的配置文件中的属性设置而来 比如 ExecutorType 的定义 defaultExecutorType 是否使用二级缓存 cacheEnabled 等

  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);
       //从代码也可以看出  一次会话 就是一个事物 一个执行器
      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();
    }
  }

4.2执行器的创建逻辑

org.apache.ibatis.session.Configuration#newExecutor() 逻辑来看都是比较简单

DefaultSqlSessionFactory
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    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 {
      executor = new SimpleExecutor(this, transaction);
    }
     //是否使用二级缓存 装饰者
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

4.3从会话到获取数据

4.3.1判断执行的类型

我们以 查询单条的逻辑selectOne 看完后续的流程

org.apache.ibatis.binding.MapperMethod#execute()
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    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()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
	  //从这里进入 就到了默认的实现类DefaultSqlSession#selectOne() 
          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;
  } 
org.apache.ibatis.session.defaults.DefaultSqlSession
@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) {
      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, Object parameter, RowBounds rowBounds) {
    try {
	//解析 XML 和 mapper 接口上的注解而来的对象 存储了一个 sql 对应的所有信息 这里就是另外一个分支逻辑了  后续文章中会详细讲解。
      MappedStatement ms = configuration.getMappedStatement(statement);
      //这里就是多态的体现了 上面初始化什么执行器 这里就是啥执行器来执行。
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

4.3.2CachingExecutor

org.apache.ibatis.executor.CachingExecutor#query() 二级缓存的执行器 逻辑也不复杂 常见的思路无非就是拿着key去缓存里面看看是否有值,这里主要还是要考虑线程安全的情况(脏读) 如果是你 你会怎么防止脏读呢?
mybatis的实现思路和git很类似,设置了**缓存暂存区 ** 只有提交后才会把暂存区的数据刷入缓存区。

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //获取查询语句所在命名空间对应的二级缓存 Cache 专门实现二级缓存定义的顶级接口
    Cache cache = ms.getCache();
    if (cache != null) {
       //是否需要清空二级缓存
      flushCacheIfRequired(ms); 
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        //没有命中缓存 就继续下个执行器 Executor delegate 这里的就是BaseExecutor
        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);
  }

//防止脏读的操作  clearOnCommit 
@Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

二级缓存是一个完整的缓存解决方案,和redis相关功能相似,
比如 存储的实现(内存 磁盘 自定义)
缓存满了要怎么做(各类淘汰算法 FIFO LRU 等)
线程安全等相关功能
mybatis 就针对二级缓存 设置了一个顶级接口Cache 责任链的方式调用 对这一系列进行完整的处理 也是一个扩容点。
mybatis源码解析一(整体了解)-鸿蒙开发者社区

4.3.2 BaseExecutor

org.apache.ibatis.executor.BaseExecutor#query() 经常被提及的一级缓存的实现 就是在这里了。 思考下 在开启二级缓存的情况下 是先进二级缓存还是一级缓存。 从这里 在联想操作系统cpu的多级缓存设计思想。 是不是都是异曲同工。

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;
  }
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 {
      //调用3个实现的执行器
      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;
  }

一级缓存总结
缓存命中条件

  • SQL与参数相同
  • 同一个会话
  • 相同的MapperStatement ID
  • RowBounds行范围相同
    触发清空缓存
  • 手动调用clearCache
  • 执行提交回滚
  • 执行update
  • 配置flushCache=true
  • 缓存作用域为Statement
    常见面试题:MyBatis集成Spring后一级缓存失效的问题?
    很多人发现,集成一级缓存后会话失效了,以为是spring Bug ,真正原因是Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,具体参见SqlSessionInterceptor。而根据前面所学,一级缓存必须是同一会话才能命中,所以在这些场景当中不能命中。
    解决方案: 给Spring 添加事物 即可。添加事物之后,SqlSessionInterceptor(会话拦截器)就会去判断两次请求是否在同一事物当中,如果是就会共用同一个SqlSession会话来解决。

4.3.3 SimpleExecutor

@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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      //后面就是设置好参数 调用到jdbc的流程了
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

已于2022-6-2 12:12:37修改
5
收藏 4
回复
举报
回复
    相关推荐