MyBatis原始碼學習筆記(一) 初遇篇

北冥有隻魚發表於2022-04-30
這個系列的文章的開篇《當我們說起看原始碼時,我們是在看什麼》在去年十月份就開始了,今天開始填這個系列的坑。MyBatis是我接觸的第一個ORM框架,也是我目前最為熟悉的ORM框架,對它一直停留在用的階段,今天嘗試來看MyBatis的內部構造。如果還不會MyBatis的,可以先去看下《假裝是小白之重學MyBatis(一)》。

那該如何看原始碼呢?

我是把MyBatis的原始碼下載下來, 茫無目的的看?這會不會迷失在原始碼中呢,我記得我剛到我當前這家公司的時候,看程式碼就是一個一個方法的看,然後感覺很頭疼,也沒看懂最後再做什麼。後面反思了一下,其實應該關注巨集觀的流程,就是這個程式碼實現了什麼功能,這些程式碼都是為了實現這個功能,不必每一個方法都一行一行的看,以方法為單位去看,這個方法從整體上來看做了什麼樣的事情,先不必過多的去關注內部的實現細節。這樣去看對程式碼大概心裡就有數了。同樣的在MyBatis這裡,這也是我第一個特別仔細研究的程式碼,所以MyBatis系列的第一篇,我們先從巨集觀上看其實現,在後面的過程中慢慢補全其細節。本篇的主線是我們在xml中寫的增刪改查語句究竟是怎麼被執行的。

參閱了很多MyBatis原始碼的資料,MyBatis的整體架構可以分為三層:

MyBatis分層

  • 介面層: SqlSession 是我們平時與MyBatis完成互動的核心介面(包括後續整合SpringFramework用到的SqlSessionTemplte)
  • 核心層: SqlSession執行的方法,底層需要經過配置檔案的解析、SQL解析,以及執行SQL時的引數對映、SQL執行、結果集對映,另外還有穿插其中的擴充套件外掛。
  • 支援層: 核心層的功能實現,是基於底層的各個模組,共同協調完成的。

搭建MyBatis的環境

搭建MyBatis的環境在《假裝是小白之重學MyBatis(一)》已經講過了,這裡只簡單在講一下:

  • 引入Maven依賴
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.6</version>
 </dependency>
  <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
        </dependency>
  • 然後來一張表
CREATE TABLE `student`  (
  `id` int(11) NOT NULL COMMENT '唯一標識',
  `name` varchar(255) ,
  `number` varchar(255) ,
  `money` int(255) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
  • 來個MyBatis的配置檔案
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--載入配置檔案-->
    <properties resource="jdbc.properties"/>
    <!--指定預設環境, 一般情況下,我們有三套環境,dev 開發 ,uat 測試 ,prod 生產 -->
    <environments default="development">
        <environment id="development">
            <!-- 設定事務管理器的管理方式  -->
            <transactionManager type="JDBC"/>
            <!-- 設定資料來源連線的關聯方式為資料池  -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/studydatabase?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--設定掃描的xml,org/example/mybatis是包的全類名,StudentMapper.xml會講-->

    <mappers>
        <!--設定掃描的xml,org/example/mybatis是包的全類名,這個BlogMapper.xml會講
         <package name = "org.example.mybatis"/> <!-- 包下批量引入 單個註冊 -->
          <mapper resource="org/example/mybatis/StudentMapper.xml"/> 
    </mappers>
    </mappers>
</configuration>
  • 來個Student類
public class Student {
    private Long id;
    private String name;
    private String number;
    private String money;
    // 省略get set 函式
}
  • 來個Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace = "org.example.mybatis.StudentMapper">
    <select id = "selectStudent" resultType = "org.example.mybatis.Student">
        SELECT * FROM STUDENT
    </select>
</mapper>
  • 來個介面
public interface StudentMapper {
    List<Student> selectStudent();
}
  • 日誌配置檔案
log4j.rootCategory=debug, CONSOLE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30
  • 開始查詢之旅
public class MyBatisDemo {
    public static void main(String[] args) throws Exception {
        Reader reader = Resources.getResourceAsReader("conf.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> studentList = studentMapper.selectStudent();
        studentList.forEach(System.out::println);
    }
}

執行之後就可以在控制檯看到如下輸出了:

MyBatis結果輸出

讓我們從SQL的執行之旅開始談起

執行過程淺析

上面的執行過程大致可以分成三步:

  • 解析配置檔案,構建SqlSessionFactory
  • 通過SqlSessionFactory 拿到SqlSession,進而獲得代理類
  • 執行代理類的方法

解析配置檔案

解析配置檔案通過SqlSessionFactoryBuilder的build方法來執行, build方法有幾個過載:

build過載

build方法

Reader指向了conf檔案, environment是環境,properties用於conf向其他properties取值。我們的配置檔案是一個xml,所以XmlConfigBuilder最終是對配置檔案的封裝。這裡我們不關注XmlBuilder是如何構建的,我們接著往下看,構建Xml物件之後,呼叫parse方法,將其轉換為MyBatis的Configuration物件:

// parseConfiguration 這個方法用於取xml標籤的值並將其設定到Configuration上
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
// 取標籤的過程,XML->Configuration
private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings); 
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers")); // 獲取mapper方法,
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  • Configuration概覽

Configuration

  • mapperElement

​ 注意我們本篇的主題是重點看我們寫在xml標籤中的sql是如何被執行的,所以我們這裡重點看parseConfiguration的mapperElement的方法。從名字上我們大致推斷,這個方法是載入mapper.xml檔案的。我們點進去看一下:

// parent 是mappers標籤
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) { // 遍歷mappers下面的結點
        if ("package".equals(child.getName())) {  // 如果是package標籤則走批量引入
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource"); // 我們本次看單個引入的方式
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource); // 載入指定資料夾下的XML
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); 
            mapperParser.parse(); // 將mapper 中的標籤值對映成MyBatis的物件
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse(); // 我們大致看下parse方法的實現
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

parent引數是mappers標籤,我們可以通過除錯驗證這一點:

mapperElement

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
在介紹的時候西安判斷該xml是否已經載入過了, 然後解析mapper標籤下的增刪改查等標籤,我們可以在configurationElement看到這一點。
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));  //該方法解析標籤
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) { // dataBaseId 用於指明該標籤在哪個資料庫下執行
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

解析標籤

parseStatementNode方法比較長,最終還是在解析Mapper.xml的select、insert、update、delete的屬性, 將解析的屬性傳遞builderAssistant.addMappedStatement()方法中去,該方法引數略多,這來我們上截圖:

MapperStatement

到此我們基本結束看構建configuration的過程,我們可以認為在這一步,Mybatis的配置檔案和Mapper.xml已經基本解析完畢。

獲取SqlSession物件

SqlSession是一個介面,有兩個主要實現類:

SqlSession概覽

我們在第一步build出來的事實上是DefaultSqlSessionFactory:

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

這裡事實上openSession也是由DefaultSqlSessionFactory來執行的,我們看下在openSession這個過程中大致做了什麼:

@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

注意這個getDefaultExecutorType, 這個事實是MyBatis分層中核心層的SQL執行器,我們接著往下看openSessionFromDataSource:

//  level 隔離級別, autoCommit 是否自動提交
//  ExecutorType 是一個列舉值: SIMPLE、REUSE、BATCH
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);
      // 返回一個執行器,我們看下newExecutor這個方法
      final Executor executor = configuration.newExecutor(tx, execType);
      // 最後構造出來SqlSession  
      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();
    }
  }
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);
  }
  // 上面是根據executorType生成對應的執行器
  // 如果開啟快取,則將其執行器包裝為另一種形式的執行器
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // interceptorChain 是一個攔截器鏈
  // 將該執行器加入到攔截器鏈中增強,這事實上是MyBatis的外掛開發。
  // 也是裝飾器模式的應用,後面會講。
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

執行增刪改查

接著我們來看我們的介面中的方法是如何執行的,

MapperProxy

StudentMapper執行selectStudent方法事實上進入的應該是對應代理的物件, 我們進入下一步, 事實上是進入了invoke方法,這個invoke方法事實上重寫的InvocationHandler的方法,InvocationHandler是JDK提供的動態代理介面,呼叫被代理的的方法,事實上是會走到這個invoke方法中,實現如下:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
         // 該方法會快取該方法,如果該在快取裡面有,則無需再次產生,裡面的methodCache是ConcurrentHashMap
         // 最終會返回MapperMethod物件呼叫invoke方法。
        // 我這裡最終的MethodInvoker是PlainMethodInvoker
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession); 
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

最終的invoke方法如下圖所示:

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  //這個execute太長,裡面根據標籤型別,來做下一步的操縱,這裡我們放截圖 
  return mapperMethod.execute(sqlSession, args);
}

方法執行

我們接著來跟executeForMany這個方法的執行:

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 {
    // 會走DefaultSqlSession的selectList下面
    result = sqlSession.selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  // 轉換結果  
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}
  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
 // 
@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 這個statement是方法引用:org.example.mybatis.StudentMapper.selectStudent
      // 通過這個key就可以從configuration獲取構建的MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      // query裡面會判斷結果是否在快取裡,我們沒有引入快取
      // 最終會走的query中的queryFromDatabase方法。
      //   queryFromDatabase 裡面會呼叫doQuery方法
      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();
    }
  }

我們這裡重點來看doQuery方法:

 @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    // 這裡我們其實已經可以看到MyBatis已經準備在呼叫JDBC了
    // Statement 就位於JDBC中
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 根據引數處理標籤中的SQL
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 產生執行SQL的Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 接著調query方法. 最終會走到PreparedStatementHandler的query方法上  
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
 // 最終執行SQL
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

PreparedStatement是JDBC,到現在就已經開始呼叫JDBC執行SQL了。resultSetHandler是對JDBC結果進行處理的處理器。

這裡我們把上面遇到的Handler大致梳理一下:

  • StatementHandler: 語句處理器
  • ResultSetHandler:結果處理器,有結果處理器就有引數處理器
  • ParameterHandler: 引數處理器,

總結一下

在MyBatis中我們寫在xml檔案中的SQL語句到底是怎樣被執行這個問題到現在已經有了答案:

  • xml中的查詢語句、屬性會被預先的載入進入Configuration中,Configuration中有MappedStatements,這是一個Map,key是標籤的id。
  • 我們在執行對應的Mapper的時候,首先要執行獲取Session,在這個過程中會經過MyBatis的攔截器,我們可以選擇在這個過程對MyBatis進行增強
  • 呼叫介面對應的方法時, 事實上呼叫的時代理類的方法,代理類會先進行引數處理,根據方法簽名獲取MappedStatement,再轉換,交給JDBC來處理。

到現在我們已經對MyBatis已經有的執行流程已經有一個大致的瞭解了,可能一些方法沒有看太細,因為講那些細節也對巨集觀執行流程沒有太大的幫助。

參考資料

相關文章