Mybatiss 框架解說

Mario發表於2023-04-02

緣由

Mybatis 框架為作為Java開發人員的必備工具,現在大多使用 mybatis-plus,但是mybatis框架中的執行原理和技術點還是值得一探究竟的。

簡述

Mybatis官網 上面介紹的非常精煉,支援自定義SQL、儲存過程以及高階對映,不過儲存過程現在已經很少使用,其餘兩點使用頻率非常的高。框架最大的特點,也是ORM最大的特點,替代開發人員編寫JDBC程式碼、引數設定、獲取結果集,開發人員只要在註解或者xml檔案中編輯SQL。減輕開發人員工作量,能夠在更有意義的事情上投入更多的精力。

正文

框架替開發人員做了很多事情,這些事情是如何做到的呢?本文會娓娓道來。

一個Mybatis應用的中有一個例項非常的關鍵,就是 SqlSessionFactory。可以從它是怎麼來的、起到了什麼作用兩個方面來詳細說說。

SqlSessionFactotry的由來

SqlSessionFactory 是一個介面類,有兩個實現類 SqlSessionManager\ DefaultSqlSessionFactory,我們先把注意力放在 DefaultSqlSessionFactory 上面,它的建立則是在 SqlSessionFactoryBuilder 中,顯然是透過構造者模式進行的建立,若一個物件建立的過程不是簡單兩行程式碼的事情,有必要使用構造器模式,將建立邏輯獨立出來,與類的定義進行隔離。

SqlSessionFactoryBuilder 中的構建邏輯最終目的就是
new DefaultSqlSessionFactory(Configuration config),所有的構建方式最終會生成 Configuration,可以是 XMLConfigBuilder.parse(),也可以使用new Configuration。兩種方式都會將開發人員對Mybatis應用的配置轉換到 SqlSessionFactory 的建立過程中。

SqlSessionFactotry的使用

SqlSessionFactory 的目的就是為了建立一個SqlSession,有多個過載的介面方法 openSession(),最終會執行到 new DefaultSqlSession(configuration, executor),configuration還是熟悉的那個,Executor 是一個介面,有幾個簡單的實現 BatchExecutor 、ReuseExecutor、SimpleExecutor、CachingExecutor,可以根據引數執行指定,兩外開發人員自定義的Interceptor也會整合到Executor實現當中,Executor 的構造需要 Configuration 和 transaction(後續單獨說明)。

SqlSession介面定義了crud 和 事務相關 快取相關 的方法。最終的執行還是會使用 Executor 實現類。比如所有的查詢方法會進入到 List<E> selectList(String statement, Object parameter, RowBounds rowBounds),statement就是開發人員指定的SQL語句識別符號,呼叫executor之前,透過 configuration.getMappedStatement(statement) 獲取到 MappedStatement 作為執行物件,參與執行過程。

Executor 執行器的query(),主要是呼叫 doQuery(),在呼叫前會建立快取key,doQuery()中存在sqlSession 級別的快取邏輯,快取沒有命中會進入 queryFromDatabase()。

queryFromDatabase() 方法會將執行結果放入快取,執行邏輯是在 doQuery(ms, parameter, rowBounds, resultHandler, boundSql) 中,此方法是一個抽象方法,進入 SimpleExecutor的抽象實現中。

Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 根據開發人員定義的sql語句時指定的 StatementType 生成對應的 StatementHandler
      // 這一點也是一個擴充套件實現
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
      // 建立 statement ,並將sql引數放入到 statement 中
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 執行查詢,並對映結果集
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
// 執行查詢,並對映結果集
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

以上就是mybatis應用中 sqlSessionFactory 大致執行邏輯,下面會從框架的擴充套件性方面說明。


擴充套件性

StatementHandler 擴充套件

其實現類 RoutingStatementHandler 採用簡單工廠和裝飾器的方式,根據 StatementType 列舉建立不同的實現。

    switch (ms.getStatementType()) {
      case STATEMENT:
        // sql 直接執行
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        // 使用 PreparedStatement 預處理引數,防止sql注入
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        // 使用 CallableStatement 方式,可以指定額外的回撥
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

Executor 擴充套件

Configuration 類中可根據 ExecutorType 建立指定的 Executor 實現類

  public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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) {
      // 裝飾模式,增加sqlSessionFactory級別的快取
      // 二級快取當中的事務也需要特別處理
      executor = new CachingExecutor(executor, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

外掛機制

SQL執行過程中,可以對其進行監控和攔截呼叫。外掛的執行邏輯入口在 configuration中,ParameterHandler、ResultSetHandler、StatementHandler、Executor 獲取過程中都會執行 interceptorChain.pluginAll() 方法。

interceptorChain負責收集開發人員定義的interceptor,並執行pluginAll() 對目標物件進行增強,採用動態代理的方式,將目標物件進行包裝。

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public static Object wrap(Object target, Interceptor interceptor) {
    // 獲取自定義的interceptor上面註解
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 獲取目標物件的介面與攔截器的註解對比,匹配到的介面
    // 下面的代理物件會根據匹配到的介面建立
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // 指定代理邏輯所在的類
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  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);
    }
  }

Mapper 代理物件

開發過程當中總會在使用依賴注入的方式引入 Mapper 物件,開發業務功能時總會定義一些UserMapper 之類的介面,方法上面使用註解SQL或者xml檔案與Mapper繫結的方式編寫SQL。

本身是一個介面,卻可以當作物件引入,必定是Mybatis提供了動態代理機制,將一個介面生成為一個代理物件,供其他類使用。

SqlSession 提供一個介面方法 <T> T getMapper(Class<T> type); 定義獲取Mapper代理物件的行為,DefaultSqlSession 中使用了 configuration.getMapper(type,this); --> mapperRegistry.getMapper(type, sqlSession); 具體實現是在 mapperRegistry當中:

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null)
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    try {
        // mapperProxyFactory 肯定會建立一個 mapperProxy 物件,代理物件mapper的執行 也會在 mapperProxy 當中
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  // 實現 InvocationHandler 介面
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    // MapperMethod 物件代表一個userMapper中的介面方法    
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // execute 方法中的執行邏輯最終會呼叫到 sqlSession 完成與資料庫的互動
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

事務

開發人員可指定事務管理 TransactionFactory,不過一般都是 JdbcTransactionFactory,使用資料庫事務,獲取sqlSession 指定是事務隔離級別,作用在JdbcTransactionFactory 建立資料庫連線時指定到連線當中。

JdbcTransactionFactory 建立的 JdbcTransaction 會參與到執行器executor的建立中,執行器中的事務方法最終會呼叫到 JdbcTransaction 執行事務呼叫。

  public void commit(boolean required) throws SQLException {
    if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
    // 清空一級快取
    clearLocalCache();
    flushStatements();
    if (required) {
      // 事務提交
      transaction.commit();
    }
  }

  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          // 事務回滾
          transaction.rollback();
        }
      }
    }
  }

總結

Mybatis框架提供了半orm機制,開發人員可以編輯SQL,剩下的工作交給框架處理,節省開發人員精力。框架的原理進行了梳理,執行流程基本說明,篇幅有限,其中的技術細節可自行延申。

相關文章