天天看點

Mybatis執行流程(配合源碼進行講解哦)一、緣起二、示例代碼三、mybatis 執行流程

一、緣起

這篇博文主要用來介紹針對 Mybatis 的 curd 操作的整個執行流程。因為 mybatis 單機是對 jdbc 操作的封裝,筆者的分析隻到 jdbc 操作流程(算是 mybatis 源碼的淺嘗辄止)。

二、示例代碼

  1、mybatis 單獨測試操作代碼

 @Test
    public void testMapper() throws IOException {
        String resource="mybatis-config.xml";
        //以流的方式擷取recource(mybatis的環境配置檔案)
        InputStream inputStream= Resources.getResourceAsStream(resource);
        //建立會話工廠
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        //通過工廠得到SqlSession
        SqlSession sqlSession= sqlSessionFactory.openSession();
        //從建立的SqlSession對象中擷取Mapper對象 (這個是核心,因為隻有拿到該對象的Mapper對象
       //(其實是Mapper接口的代理MapperProxy對象執行個體))
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
 
        int bookId = 1000;
        BookExample bookExample = new BookExample();
        bookExample.createCriteria().andIdEqualTo(bookId);
        List<Book> books = bookMapper.selectByExample(bookExample);
 
    }

           

   2、spring 與 mybatis 的整合操作代碼

 @Autowired
 private BookMapper bookDao;
 
 @Test
 public void testQueryById() throws Exception {
  int bookId = 1000;
  BookExample bookExample = new BookExample();
  bookExample.createCriteria().andIdEqualTo(bookId);
  List<Book> books = bookDao.selectByExample(bookExample);
 }

           

3、 總結

如上兩種方式前者使用代碼顯示的從 SqlSession 對象中擷取 Mapper 代理對象,後者則是在 spring 容器啟動後将 Mapper 對象建立放入 Spring 容器中進行托管。這裡有個疑問在于我們編寫的 XxxMapper 為接口,但是在使用過程中初始化了其相應的 Mapper 代理對象。這裡我們就來分析一下。

3.1、MapperProxy 對象

這裡不賣關子了,其實 Mybatis 為我們動态的生成了 XxxMapper 對象的的代理對象類為 MapperProxy,熟悉 JDK 動态代理的同學都知道接口中所有的實作都是調用實作了 InvocationHandler 對象的 invoke() 方法,我們所有對 XxxMapper 接口中方法的調用最終都會調用 invoke() 方法。上面的兩個測試(一個單獨測試,一個 spring 整合測試)該對象生成過程之間略有差異,前者是使用 DefaultSqlSession 的 getMapper 方法來生成,而後者則是使用 SqlSessionTemplate 對象的 getMapper() 方法生成。這裡我們來對該 MapperProxy 對象初始化來做分析。

    public <T> T getMapper(Class<T> type) {
        //通過Configuration對象擷取其中的mapper
        return this.getConfiguration().getMapper(type, this);
    }

           

configuration 對象是我們 mybatis 的所有配置的對象,通過調用其 getMapper() 方法來擷取 MapperProxy,但是實際操作是被委托給了 MapperRegistry 對象。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  //通過configuration對象中的MapperRegistry對象調用其中的getMapper方法
  return mapperRegistry.getMapper(type, sqlSession);
}

           

mapperRegistry 根據對應的 XxxMapper 類 class 資訊以及對應的 mybatis 會話 sqlSession 對象來擷取 dao 層代理對象。還使用到了我們初始化 Mapper.xml 對象的時候将其以 XxxMapper className 為 key,MapperProxyFactory 為 value 的形式存放在 Map 類型的結構對象 knownMappers 中,是以此處擷取到對應的 MapperProxyFactory 對象(該對象中包含了 XxxMapper 接口類資訊),通過 Jdk 動态代理生成 XxxMapper 接口的動态 dialing 對象

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   //從map容器中擷取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 {
      //調用map中擷取的MapperProxyFactory對象 執行個體化Mapper動态代理對想
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
 
 
//JDK動态代理執行個體化dao接口對象
 
 protected T newInstance(MapperProxy<T> mapperProxy) {
    //jdk動态代理
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
 
  public T newInstance(SqlSession sqlSession) {
    //采購員别廢話吧MapperProxy對象
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    //dao接口執行個體化
    return newInstance(mapperProxy);
  }

           

三、mybatis 執行流程

     1、mybatis 的架構設計圖

Mybatis執行流程(配合源碼進行講解哦)一、緣起二、示例代碼三、mybatis 執行流程

(嘻嘻,圖是盜了 csdn 一位大牛的圖 , 盜圖位址:https://blog.csdn.net/luanlouis/article/details/40422941)

 2、mybatis 的執行流程圖

Mybatis執行流程(配合源碼進行講解哦)一、緣起二、示例代碼三、mybatis 執行流程

 3、mybatis 執行流程時序圖

Mybatis執行流程(配合源碼進行講解哦)一、緣起二、示例代碼三、mybatis 執行流程

 4、Mybatis 的核心元件

以相關的執行流程梳理出來的一個重要的 java 對象

  • SqlSession            作為 MyBatis 工作的主要頂層 API,表示和資料庫互動的會話,完成必要資料庫增删改查功能
  • Executor              MyBatis 執行器,是 MyBatis 排程的核心,負責 SQL 語句的生成和查詢緩存的維護
  • StatementHandler   封裝了 JDBC Statement 操作,負責對 JDBC statement 的操作,如設定參數、将 Statement 結果集轉換成 List 集合。
  • ParameterHandler   負責對使用者傳遞的參數轉換成 JDBC Statement 所需要的參數,
  • ResultSetHandler    負責将 JDBC 傳回的 ResultSet 結果集對象轉換成 List 類型的集合;
  • TypeHandler          負責 java 資料類型和 jdbc 資料類型之間的映射和轉換
  • MappedStatement   MappedStatement 維護了一條 <select|update|delete|insert> 節點的封裝,
  • SqlSource            負責根據使用者傳遞的 parameterObject,動态地生成 SQL 語句,将資訊封裝到 BoundSql 對象中,并傳回
  • BoundSql             表示動态生成的 SQL 語句以及相應的參數資訊
  • Configuration        MyBatis 所有的配置資訊都維持在 Configuration 對象之中。

5、mybatis 執行流程的分析

二中講述了 Mapper 接口的生成過程中提到了 MapperProxy 對象,所有 Mapper 接口的調用都最終會到達 MapperProxy 的 invoke() 方法,這裡也是我們進行 mybatis 執行過程分析的入口。

 //所有dao層接口的調用最終會到達該方法中進行處理
 // proxy為動态代理對象 Method為Dao接口層最終調用的具體方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //根據method從緩存中擷取對應的MapperMethod對象,
    //如果沒有建立MapperMethod 并将其放入緩存中并傳回
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //最終方法的調用交于MapperMethod對象
    return mapperMethod.execute(sqlSession, args);
  }

           

從上面的代碼可以看出擷取 MapperMethod 對象, 該對象通過 execute() 來執行真正的資料庫調用。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else if (SqlCommandType.FLUSH == command.getType()) {
        result = sqlSession.flushStatements();
    } else {
      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;
  }

           

總結:上述代碼主要是針對具體的 sql 方法 insert|delete|update|select 來進行不同的分支操作,分支操作主要有兩點:1、将對應的參數轉換為 sql 參數,2、根據不同的 curd 操作分别調用 SqlSession 對象的對應方法(insert(),update(),delete() 等...)。這裡以查詢為例子調用 DefauleSqlSession 的 selectList() 方法

 //以查詢為例,查詢集合清單
//1、statement 辨別XxxMapper.xml檔案的某一個curd方法的id 例如 
//select id="selectByExample" parameterType="bookExample" resultMap="BaseResultMap">
//中的statement為 全限定類名+selectByExample構成了一個statement
//2、parameter 參數
//3、RowBounds  分頁相關的對象  
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //根據statement (statement的唯一辨別)
      MappedStatement ms = configuration.getMappedStatement(statement);
      //最終調用Executor執行器元件來實作對應的sql查詢操作
      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();
    }
  }

           

在上述代碼中出現了新的對象 Executor,該對象是 mybatis 的執行器我們實際進行的增删改查都是通過這個 Executor 來進行的,

主要有兩個實作類:BaseExecutor 和 CachingExecutor,CachingExecutor 用于二級緩存,而 BaseExecutor 則用于一級緩存及

基礎的操作 BaseExecutor 對象下面也有三個子類分别是:

  • SimpleExecutor 是最簡單的執行器,根據對應的 sql 直接執行即可,不會做一些額外的操作;
  • BatchExecutor 執行器,顧名思義,通過批量操作來優化性能。通常需要注意的是批量更新操作,由于内部有緩存的實作,使用完成後記得調用

    flushStatements

    來清除緩存。
  • ReuseExecutor 可重用的執行器,重用的對象是 Statement,也就是說該執行器會緩存同一個 sql 的 Statement,省去 Statement 的重新建立,優化性能。内部的實作是通過一個 HashMap 來維護 Statement 對象的。由于目前 Map 隻在該 session 中有效,是以使用完成後記得調用

    flushStatements

    來清除 Map。
可以在mybatis的配置文章中進行設定使用什麼類型的Executor 預設是SimpleExecutor
<settings>
    <!--SIMPLE、REUSE、BATCH-->
      <setting name="defaultExecutorType" value="REUSE"/>
</settings>
    

           

有關 Executor 對象的相關隻是參考:mybatis 的 Executor 相關說明

//調用執行器的query方法來執行資料庫的底層操作。其中最後一個參數為ResultHandler對查詢出來的
//結果集合進行處理的對象
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //擷取包含動态sql的BoundSql對象
    BoundSql boundSql = ms.getBoundSql(parameter);
    //根據Statamenr、參數,RowBounds(分頁對象),boundSql(動态sql) 建立一級緩存所使用的key
    //在後面對查詢出來的結果進行緩存的時候使用
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //查詢操作
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }
 
 
 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.");
    }
    //當queryStack ==0 并且配置了flushCache 為true 則每次調用的時候重新整理緩存
    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的作用
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      //如果配置了本地緩存的範圍為Statement 而非SqlSession 也需要情況緩存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

           

抛出緩存的問題不表,我們關注的一下執行器的 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 {
      //調用Executor對象的doQuery方法 在BaseExecutor的doQuery()為抽象方法,
      //需要使用子類的實作調用,這裡預設使用SimpleExecutor對象 
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //移除占位符(感覺此處多此一舉)
      localCache.removeObject(key);
    }
    //将結果存放在一級緩存中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      //對于存儲過程,存放其參數資訊在localOutputParameterCache中
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

           

查詢 SimpleExecutor 的 doQuery() 方法

//SimpleExecutor子類執行 
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      //擷取mybatis的配置資訊對象Configuration
      Configuration configuration = ms.getConfiguration();
      //使用configuration執行個體化一個StatementHandler對象 該對象主要是用來建構Statement對象的
      //StatementHandler的作用就是先通過prepare方法建構一個Statement對象,然後再調用其他方法對 
      //Statement對象進行處理
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //使用ResultHandler構造Statement對象stmt
      stmt = prepareStatement(handler, ms.getStatementLog());
      //調用Statement query()方法
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

           

調用 StatementHandler 對象 子類 PreparedStatementHandler 對象, 我們最常用的也是預編譯的 sql statement 處理,是以調用其 query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //jdbc statement的execute() 這裡擷取到的結果對象Resultset 被存儲了供下面代碼進行
    //結果映射處理的時候使用
    ps.execute();
    //使用ResultsetHandler 來處理結果映射
    return resultSetHandler.<E> handleResultSets(ps);
  }

           

結果映射的處理實作分析, 調用 DefaultResultSetHandler 對象來對相應的結果進行解析處理

ResultSetHandler 負責處理兩件事:

(1)handleResultSets(Statement stmt) 處理 Statement 執行後産生的結果集,生成結果清單

(2) handleOutputParameters(CallableStatement cs) 處理存儲過程執行後的輸出參數

這裡我們隻探究普通 sql 的執行過程,不探究存儲過程的研究

  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
 
    //結果集合轉換成的java對象的封裝
    final List<Object> multipleResults = new ArrayList<Object>();
 
    int resultSetCount = 0;
    //擷取結果集合相關資訊 并包裝成ResultSetWrapper對象,其包含了結果集合的相關資訊
    //一般情況下都是擷取一個結果集
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    
    //擷取對應的xxxMapper.xml方法的某個标簽下配置的resultMap屬性(可以多個,是以為集合)
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    
    int resultMapCount = resultMaps.size();
    //校驗resultMap是否存在
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      //擷取查詢結果集對應的映射關系ResultMap對象,對應我們xxxMapper.xml中的reusltMap标簽 
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //将集合集合根據reusltMap的映射關系,映射為實體對象并存放到multipleResults集合中
      handleResultSet(rsw, resultMap, multipleResults, null);
      //擷取下一個結果集對象資訊
      rsw = getNextResultSet(stmt);
      //清空相關的緩存資訊
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
 
    //resultsets則是對擷取對應的xxxMapper.xml方法的某個标簽下配置的resultsets屬性的封裝
    //筆者沒使用過該resultset對象
    String[] resultSets = mappedStatement.getResulSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
    //傳回集合對象
    return collapseSingleResultList(multipleResults);
  }

           

作者:liushangzaibeijing

來源連結:

https://blog.csdn.net/liushangzaibeijing/article/details/86550077

Mybatis執行流程(配合源碼進行講解哦)一、緣起二、示例代碼三、mybatis 執行流程