MyBatis 之源碼淺讀

  • 2019 年 10 月 25 日
  • 筆記

環境簡介與入口

記錄一下嘗試閱讀Mybatis源碼的過程,這篇筆記是我一邊讀,一遍記錄下來的,雖然內容也不多,對Mybatis整體的架構體系也沒有摸的很清楚,起碼也能把這個過程整理下來,這也是我比較喜歡的一種學習方式吧

單獨Mybatis框架搭建的環境,沒有和其他框架整合

入口點的源碼如下:

@Test  public void test01() {     try {      this.resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");      // 2. 創建SqlSessionFactory工廠      this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);      // 3. 創建sqlSession      // todo 怎麼理解這個sqlSession? 首先它是執行緒級別的,執行緒不安全, 其次它裡面封裝了大量的CRUD的方法      this.sqlSession = factory.openSession();        IUserDao mapper = this.sqlSession.getMapper(IUserDao.class);      List<User> all = mapper.findAll();      for (User user : all) {          System.out.println(user);      }      // 事務性的操作,自動提交      this.sqlSession.commit();      // 6, 釋放資源      this.sqlSession.close();      this.resourceAsStream.close();    } catch (IOException e) {     e.printStackTrace();    }  }  

構建SqlSessionFactory

首先跟進這個,看看如何構建SqlSessionFactory對象

 this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);

這個SqlSessionFactoryBuilder類的存在很簡單,取名也叫他構建器,Mybatis的官網是這樣解釋它的,這個類可以被實例化(因為它有且僅有一個默認的無參構造),使用它的目的就是用來創建多個SqlSessionFactory實例,最好不要讓他一直存在,進而保證所有用來解析xml的資源可以被釋放

所以跳過對這個構建器的關注,轉而看的build()方法

首先會來到這個方法,直接可以看到存在一個XML配置解析器,這其實並不意外,畢竟現在是MyBatis是孤軍一人,就算我們使用的是註解開發模式,不也得存在主配置文件不是?

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {      try {        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);        return build(parser.parse());      } catch (Exception e) {        throw ExceptionFactory.wrapException("Error building SqlSession.", e);      } finally {        ErrorContext.instance().reset();        try {          inputStream.close();        } catch (IOException e) {          // Intentionally ignore. Prefer previous error.        }      }    }

接著看看parser.parse(),它裡面會解析主配置文件中的資訊,解析哪些資訊呢? 源碼如下: 很清楚的看到,涵蓋mybatis配置文件中的所有的標籤

 private void parseConfiguration(XNode root) {      try {        //issue #117 read properties first        propertiesElement(root.evalNode("properties"));        Properties settings = settingsAsProperties(root.evalNode("settings"));        ...        typeHandlerElement(root.evalNode("typeHandlers"));        mapperElement(root.evalNode("mappers"));      } catch (Exception e) {        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);      }    }

第一個問題: 解析的配置資訊存在哪裡呢? 其實存放在一個叫Configuration的封裝類中, 這個上面的解析器是XMLConfigBuilder 這個封裝類的保存者是它的父類BaseBuilder

繼續跟進build()方法,源碼如下:

 public SqlSessionFactory build(Configuration config) {      return new DefaultSqlSessionFactory(config);    }

小結: 至此也就完成了DefaultSqlSessionFactory()的構建,回想下,這個構建器真的太無私了,犧牲了自己,不僅僅創建了默認的SqlSessionFactory,還將配置文件的資訊給了他

打開SqlSession

創建完成SqlSession工廠的創建, 我們繼續跟進this.sqlSession = factory.openSession(); , 從工廠中獲取一個SqlSession

跟進幾個空殼方法,我們很快就能來到下面的方法:

  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();      }    }

什麼是SqlSession呢? 注釋是這麼說的,他是MyBatis在java中主要幹活的介面,通過這個介面,你可以執行命令(它裡面定義了大量的 諸如selectList類似的方法),獲取mapper,合併事務

The primary Java interface for working with MyBatis.  Through this interface you can execute commands, get mappers and manage transactions.

通過上面的我貼出來的函數,大家可以看到,通過事務工廠實例化了一個事物Transaction,那麼問題來了,這個Transaction又是什麼呢? 注釋是這麼解釋的: Transaction 包裝了一個資料庫的連接,處理這個連接的生命周期,包含: 它的創建,準備 提交/回滾 和 關閉

緊接著創建執行器:configuration.newExecutor(tx, execType),默認創建的是CachingExecutor它維護了一個SimpleExecutor, 這個執行器的特點是 在每次執行完成後都會關閉 statement 對象

關於mybatis的執行器,其實挺多事的,打算專門寫一篇筆記

繼續看new DefaultSqlSession()我們得知,這個sqlSession的默認實現類是DefaultSqlSession

第三個參數autocommit為false, 這也是為什麼我們如果不手動提交事務時,雖然測試會通過,但是事務不會被持久化的原因

小結: 當前函數是 openSession(), 如果說它是打開一個session,那跟沒說是一樣的,通過源碼我們也看到了,這一步其實是Mybatis將 資料庫連接,事務,執行器進行了一下封裝然後返回給程式設計師

獲取Mapper -- maperProxy

我們交給mybatis的mapper是一個介面,看看Mybatis是如何實例化我們的結果,返回給我們一個代理對象的

跟進源碼:IUserDao mapper = this.sqlSession.getMapper(IUserDao.class); ,經過一個空方法,我們進入Configuration類中的函數如下:

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

從mapperRegistry中獲取mapper,他是Configuration屬性如下: 可以看到這個mapperRegistry甚至包含了Configuration,甚至還多了個 knownMappers

那麼問題來了,這個knownMappers是幹啥呢? 我直接說,這個map就是在上面解析xml配置文件時,存放程式設計師在<mappers>標籤下配置的<maper>

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);    // 詳情:  public class MapperRegistry {      private final Configuration config;      private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

繼續跟進這個getMapper()如下: 並且我們在配置文件中是這樣配置的

<mappers>      <mapper class="com.changwu.dao.IUserDao"/>  </mappers>
  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 {        return mapperProxyFactory.newInstance(sqlSession);      } catch (Exception e) {        throw new BindingException("Error getting mapper instance. Cause: " + e, e);      }    }

所以不難想像,我們肯定能獲取到結果,通過上面的程式碼我們能看到,獲取到的對象被強轉成了MapperProxyFactory類型,它的主要成員如下: 說白了,這個 map代理工廠是個輔助對象,它是對程式設計師提供的mapper結果的描述,同時內置使用jdk動態代理的邏輯為mapper創建代理對象

public class MapperProxyFactory<T> {      private final Class<T> mapperInterface;    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();    ...      protected T newInstance(MapperProxy<T> mapperProxy) {      return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);      }

說到了為mapper創建動態代理,就不得不去看看是哪個類充當了動態代理的需要的InvoketionHandler -- 這個類是mybatis中的MapperProxy, 沒錯它實現了InvocationHandler介面,重寫了invoke()邏輯,源碼如下:

@Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  try {    if (Object.class.equals(method.getDeclaringClass())) {      return method.invoke(this, args);    } else if (isDefaultMethod(method)) {      return invokeDefaultMethod(proxy, method, args);    }  } catch (Throwable t) {    throw ExceptionUtil.unwrapThrowable(t);  }  final MapperMethod mapperMethod = cachedMapperMethod(method);  return mapperMethod.execute(sqlSession, args);  }

小結: 至此當前模組的獲取mapper對象已經完結了,我們明明白白的看到了MyBatis為我們的mapper使用jdk的動態代理創建出來代理對象, 這也是為什麼我們免去了自己寫實現類的粗活

執行Map -- maperProxy

上一個模組我們知道了Mybatis為我們創建出來了mapper介面的代理對象,那當我們獲取到這個代理對象之後執行它的mapper.findAll();實際上觸發的是代理對象的invoke()方法

所以說,接著看上面的MapperProxy的invoke()方法:

@Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  try {    if (Object.class.equals(method.getDeclaringClass())) {      return method.invoke(this, args);    } else if (isDefaultMethod(method)) {      return invokeDefaultMethod(proxy, method, args);    }  } catch (Throwable t) {    throw ExceptionUtil.unwrapThrowable(t);  }  final MapperMethod mapperMethod = cachedMapperMethod(method);  return mapperMethod.execute(sqlSession, args);  }

上面的方法我們關注兩個地方,第一個地方就是final MapperMethod mapperMethod = cachedMapperMethod(method);,見名知意: 快取MapperMethod

第一個問題: 這個MapperMethod是什麼? 它其實是Mybatis為sql命令+方法全限定名設計的封裝類

 */  public class MapperMethod {      private final SqlCommand command;    private final MethodSignature method;

說白了,就是想為MapperProxy保存一份map格式的資訊,key=方法全名 value=MapperMethod(command,method),存放在MapperProxy的屬性methodCache中

comand -> "name=com.changwu.dao.IUserDao,fingAll"

method -> "result= public abstract java.util.List.com.changwu.dao.IUserDao.findAll()"

為什麼要給這個MapperProxy保存這裡呢? 很簡單,它是mapper的代理對象啊,程式設計師使用的就是他,在這裡留一份method的副本,再用的話多方便?

完成快取後,繼續看它如何執行方法:,跟進mapperMethod.execute(sqlSession, args)

因為我使用的是@Select("select * from user"),所以一定進入下面的result = executeForMany(sqlSession, args);

   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 SELECT:          if (method.returnsVoid() && method.hasResultHandler()) {            executeWithResultHandler(sqlSession, args);            result = null;          } else if (method.returnsMany()) {            result = executeForMany(sqlSession, args);          ...  }

所以我們繼續關注這個 executeForMany(sqlSession, args);方法,看他的第一個參數是sqlSession,也就是我們的DefaultSqlSession,他裡面存在兩大重要對象: 1是configuration 配置對象, 2是Executor 執行器對象

繼續跟進:

  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {      List<E> result;      Object param = method.convertArgsToSqlCommandParam(args);      if (method.hasRowBounds()) {        RowBounds rowBounds = method.extractRowBounds(args);        result = sqlSession.selectList(command.getName(), param, rowBounds);      } else {        result = sqlSession.selectList(command.getName(), param);      }

我們在繼續跟進這個selectList方法之前,先看看這個command.getName()是啥? 其實我們上面的程式碼追蹤中有記錄: 就是name=com.changwu.dao.IUserDao,fingAll

繼續跟進去到下面的方法:

這個方法就比較有趣了,首先來說,下面的入參都是什麼

statement = "com.changwu.dao.IUserDao.findAll" parameter=null

第二個問題:MappedStatement是什麼? 它是一個對象,維護了很多很多的配置資訊,但是我們關心它裡面的兩條資訊,這其實可以理解成一種方法與sql之間的映射,如下圖

mappedStatement

  @Override    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {      try {        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();      }    }

前面閱讀時,我們知道Mybatis為我們創建的默認的執行器 Executor是CachingExecutor,如下圖

CachingExecutor

繼續跟進,主要做了下面三件事, 獲取到綁定的sql,然後調用SimpleExecutor快取key,然後繼續執行query()方法

 @Override    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {      BoundSql boundSql = ms.getBoundSql(parameterObject);      CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);      return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);    }

接著調用SimpleExecutor的query()方法,然後我們關注SimpleExecutor的doQuery()方法,源碼如下

 @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());        return handler.query(stmt, resultHandler);      } finally {        closeStatement(stmt);      }    }

我們注意到.在SimpleExcutor中創建的了一個 XXXStatementHandler這樣一個處理器, 所以我們的只管感覺就是,sql真正執行者其實並不是Executor,而是Executor會為每一條sql的執行重新new 一個 StatementHandler ,由這個handler去具體的執行sql

關於這個StatementHandler到底是是何方神聖? 暫時了解它是Mybatis定義的一個規範介面,定義了如下功能即可

public interface StatementHandler {    // sql預編譯, 構建statement對象    Statement prepare(Connection connection, Integer transactionTimeout)        throws SQLException;    // 對prepare方法構建的預編譯的sql進行參數的設置    void parameterize(Statement statement)        throws SQLException;    // 批量處理器    void batch(Statement statement)        throws SQLException;    // create update delete    int update(Statement statement)        throws SQLException;    // select    <E> List<E> query(Statement statement, ResultHandler resultHandler)        throws SQLException;      <E> Cursor<E> queryCursor(Statement statement)        throws SQLException;    // 獲取sql的封裝對象    BoundSql getBoundSql();    // 獲取參數處理對象    ParameterHandler getParameterHandler();    }  

了解了這個StatementHandler是什麼,下一個問題就是當前我們創建的默認的statement是誰呢? routingStatementHandler如下圖

https://img2018.cnblogs.com/blog/1496926/201910/1496926-20191025220026439-543525632.png

創建preparedStatement

馬上馬就發生了一件悄無聲息的大事!!!根據現有的sql等資訊,構建 PreparedStatement,我們關注這個prepareStatement(handler, ms.getStatementLog());方法,通過調試我們得知,prepareStatement()是RoutingStatementHandler的抽象方法,被PreparedStatementHandler重寫了,所以我們去看它如何重寫的,如下:

@Override  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {  ErrorContext.instance().sql(boundSql.getSql());  Statement statement = null;  try {    statement = instantiateStatement(connection);

我們關注這個instantiateStatement()方法, 並且進入它的connection.prepareStatement(sql);方法,如下圖:

preparedStatement

純潔的微笑... 見到了原生JDK, jdbc的親人...

創建完事這個 preparedStatement,下一步總該執行了吧...繞這麼多圈...

我們回到上面程式碼中的return handler.query(stmt, resultHandler);準備執行,此時第一個參數就是我們的剛創建出來的PreparedStatement, 回想一下,上面創建的這個默認的statement中的代表是PreparedStatementHandler,所以,我們進入到這個StatementHandler的實現類RountingStatementHandler中,看他的query()方法

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

調用RountingStatementHandler中維護的代表的StatementHandler也就是PreparedStatementHandler的query()方法,順勢跟進去,最終會通過反射執行jdbc操作,如圖, 我圈出來的對象就是我們上面創建出來的preparedStatement

执行preparedStatement

提交事務

跟進conmit()方法,分成兩步

  • 清空快取
  • 提交事務

清空快取是在CachingExecutor中調用了SimpleExecutor簡單執行器的方法commit(required)

  @Override    public void commit(boolean required) throws SQLException {      delegate.commit(required);      tcm.commit();    }

接在SimpleExecutor的父類BaseExecutor中完成

  @Override    public void commit(boolean required) throws SQLException {      if (closed) {        throw new ExecutorException("Cannot commit, transaction is already closed");      }      clearLocalCache();      flushStatements();      if (required) {        transaction.commit();      }    }

提交事務的操作在tcm.commit();中完成

本文到這裡也就行將結束了,如果您覺得挺好玩的,歡迎點贊支援,有錯誤的話,也歡迎品評指出

Tags: