1. 程式人生 > 程式設計 >帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——構建部分

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——構建部分

瞭解 Mybatis

MyBatis 的前身是 Apache 的開源專案 iBatis。MyBatis 消除了幾乎所有的 JDBC 程式碼和引數的手工設定以及對結果集的檢索封裝,是一個支援普通 SQL 查詢,儲存過程和高階對映的基於 Java 的優秀持久層框架。

MyBatis 配置

當閱讀原始碼的時候我們不能深陷一些細節,我們應該先鳥瞰全貌,這樣能夠幫助你從高維度理解框架。

因為這篇文章主要涉及配置檔案對應的配置物件的初始化和構建,所以執行部分先不做介紹。我們首先放一張平時我們使用 Mybatis 的時候會編寫的兩個重要配置檔案——mybatis-config.xmlxxxMapper.xml

這裡我們預設 mybatis 的配置檔案為 mybatis-config.xml

在 mybatis-config.xml 配置檔案中,我們會有一個專門的 <mappers>標籤映射了相關的 mapper 對映檔案。

其實,Mybatis的構建流程就是:對配置檔案解析成配置物件裝入記憶體中

Mybatis是如何將配置檔案解析成配置物件的

首先我們來思考一個問題:這個配置物件什麼時候會被使用到?

我們知道在 mybatis-config.xml 中配置了一些型別處理器,型別別名,mappers,資料庫連線資訊等等,而這些東西在每次資料庫連線進行 CRUD 操作的時候都需要用到,也就是在每次 SQL會話

中我們需要用到。

而在 Mybatis 中使用了一個 SqlSession 介面來表示和規範了 Sql會話,我們需要通過專門的 SqlSessionFactory 去建立,這裡面是一種工廠模式。這裡我簡單畫一下 UML 圖,你可以回顧一下 工廠模式,但這不是這篇文章的重點。

Mybatis使用了工廠模式還不止,在構造 SqlSessionFactory 的時候還使用了 SqlSessionFactoryBuilder 去構建 SqlSessionFactory 也就是使用了構建者模式。而又因為在建立 SqlSession 的時候我們需要傳入我們的配置物件 Configuration,而我們知道 mybatis-config.xml 配置檔案中有許多標籤,也就意味著當我們構造一個 Configuration

物件的時候會帶有很多欄位的解析,那麼整個 Configuration 物件的構建是非常複雜的。在 Mybatis 中使用了 構建者模式 來解決這個問題。我們可以看一下原始碼。

// 這是在SqlSessionFactoryBuilder類中
// 在 SqlSessionFactoryBuilder 中會有很多build構造工廠的方法
// 其中這裡是主線,因為其他build方法都會呼叫此方法
public SqlSessionFactory build(InputStream inputStream,String environment,Properties properties) {
try {
  // 通過配置檔案解析成的流去建立 
  // 構建Configuration物件的 builder類 XmlConfigBuilder
  // 之後會呼叫parse方法構建 Configuration 物件
  XMLConfigBuilder parser = new XMLConfigBuilder(inputStream,environment,properties);
  // 最終會呼叫引數為Configuration的build方法
  // 進行最終的SqlSessionFactory的構建
  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.
  }
}
}
複製程式碼

這樣我們就可以畫出一個簡單的流程圖了。

沿著主線走看 Mybatis 如何解析配置檔案的

由上面的分析我們可以知道:XMLConfigBuilder 類中的 parse() 方法進行了 Configuration 物件的解析和構建

我們來沿著這條路線進去看看底層原理是什麼樣的。

public Configuration parse() {
// 如果已經解析過了,報錯
if (parsed) {
  throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 根節點是configuration
// 解析還在這裡
// 我需要在這裡解釋一下
// "/configuartion" 這個其實是xpath語法
// mybatis封裝了 xpath解析器去解析 xml
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

//解析配置
private void parseConfiguration(XNode root) {
try {
  //分步驟解析
  //issue #117 read properties first
  //1.properties
  propertiesElement(root.evalNode("properties"));
  //2.型別別名
  typeAliasesElement(root.evalNode("typeAliases"));
  //3.外掛
  pluginElement(root.evalNode("plugins"));
  //4.物件工廠
  objectFactoryElement(root.evalNode("objectFactory"));
  //5.物件包裝工廠
  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  //6.設定
  settingsElement(root.evalNode("settings"));
  // read it after objectFactory and objectWrapperFactory issue #631
  //7.環境
  environmentsElement(root.evalNode("environments"));
  //8.databaseIdProvider
  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  //9.型別處理器
  typeHandlerElement(root.evalNode("typeHandlers"));
  //10.對映器
  mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
  throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e,e);
}
}
複製程式碼

看到這些有沒有覺得很熟悉?

其實就是 配置檔案中的一些標籤配置。我們畫張圖來對應一下就一目瞭然了。

比較重要的 mappers

如上圖所示,在整個 Configuration 配置物件的構建過程中需要涉及到很多標籤的解析,所以 Mybatis 巧妙地利用了 構建者模式,而這麼多配置資訊在這篇文章中我不能一一去進行原始碼分析(有很多都是細枝末節的東西,我們只需要大概知道幹什麼就行了),所以我挑了最重要的 <mappers> 標籤的解析去進行原始碼分析。

我們再次進入原始碼檢視,這回是 XmlConfigBuilder 中的 mapperElement(XNode parent) 方法。當然我們最好對照著配置資訊格式去看

<mappers>
  <!-- 這幾種配置方式 結合下面原始碼看 -->
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <package name="org.mybatis.builder"/>
</mappers>
複製程式碼
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
  for (XNode child : parent.getChildren()) {
    if ("package".equals(child.getName())) {
      // 自動掃描包下所有對映器
      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);
        // 對映器比較複雜,呼叫XMLMapperBuilder
        // 注意在for迴圈裡每個mapper都重新new一個XMLMapperBuilder,來解析
        // 注意構建者裡面還傳入了 configuration
        // 也就是說 mapper對映檔案 對應的配置物件也需要封裝在 configuration中
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,configuration,resource,configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null && url != null && mapperClass == null) {
        // 使用絕對url路徑
        ErrorContext.instance().resource(url);
        InputStream inputStream = Resources.getUrlAsStream(url);
        // 對映器比較複雜,呼叫XMLMapperBuilder
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,url,configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null && url == null && mapperClass != null) {
        // 使用java類名
        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.");
      }
    }
  }
}
}
複製程式碼

我們會對 mappers 標籤裡面的子標籤進行遍歷,對於除了 package 的三種資源標識(resource,class)來說,每個 mapper 子標籤都會構建一個 XMLMapperBuilder 去構建解析對應的 mapper 對映配置檔案。其實這些 資源標誌 就是讓程式去尋找到對應的 xxxMapper.xml 對映檔案,然後同樣適用構建者模式去構建 xxxMapper.xml 對應的配置物件

我們來看一下 XmlMapperBuilder 構建者是如何構建相應的 “Mapper” 配置物件的。

public void parse() {
// 如果沒有載入過再載入,防止重複載入
if (!configuration.isResourceLoaded(resource)) {
  //主線在這裡 配置 mapper
  configurationElement(parser.evalNode("/mapper"));
  // 標記一下,已經載入過了
  configuration.addLoadedResource(resource);
  // 繫結對映器到namespace
  bindMapperForNamespace();
}
// 可以忽略  
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}

private void configurationElement(XNode context) {
try {
  //1.配置namespace
  // 這步驟也是挺關鍵 先記住 namespace這個東西
  String namespace = context.getStringAttribute("namespace");
  if (namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  builderAssistant.setCurrentNamespace(namespace);
  // 快取 可以先不管
  //2.配置cache-ref
  cacheRefElement(context.evalNode("cache-ref"));
  //3.配置cache
  cacheElement(context.evalNode("cache"));
  //4.配置parameterMap(已經廢棄,老式風格的引數對映)
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // mybatis 非常非常重要的功能
  //5.配置resultMap(高階功能)
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  //6.配置sql(定義可重用的 SQL 程式碼段)
  sqlElement(context.evalNodes("/mapper/sql"));
  //7.配置select|insert|update|delete
  // 這裡是真正的主線
  // 這裡會根據前面的sql片段建立在Mapper中真正的配置物件 MappedStatement
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e,e);
}
}

// 傳入 select|insert|update|delete 標籤的 節點列表進行構建 Statement 
private void buildStatementFromContext(List<XNode> list) {
  // 判斷DatabaseId
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list,configuration.getDatabaseId());
  }
  // 都是呼叫這個方法
  buildStatementFromContext(list,null);
}

private void buildStatementFromContext(List<XNode> list,String requiredDatabaseId) {
for (XNode context : list) {
  // 構建所有語句,一個mapper下可以有很多select
  // 這裡又使用了構造者模式
  // 語句比較複雜,核心都在這裡面,所以呼叫XMLStatementBuilder
  final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration,builderAssistant,context,requiredDatabaseId);
  try {
    // 主線  
    // 核心XMLStatementBuilder.parseStatementNode
    statementParser.parseStatementNode();
  } catch (IncompleteElementException e) {
      // 如果出現SQL語句不完整,把它記下來,塞到configuration去
    configuration.addIncompleteStatement(statementParser);
  }
}
}
複製程式碼

上面那麼長的一大串程式碼其實就是一個鏈式呼叫。我們畫一下流程便於你理解。

接下來就到了 XMLStatementBuilder 這個類中去構建 MappedStatement 物件了。

// 緊接著上面的解析構建方法
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    // 如果databaseId不匹配,退出
    if (!databaseIdMatchesCurrent(id,databaseId,this.requiredDatabaseId)) {
      return;
    }

    //暗示驅動程式每次批量返回的結果行數
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超時時間
    Integer timeout = context.getIntAttribute("timeout");
    //引用外部 parameterMap,已廢棄
    String parameterMap = context.getStringAttribute("parameterMap");
    // 前面三個不太重要
    //引數型別 這個在引數對映的時候挺重要
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //引用外部的 resultMap(高階功能) 非常重要了
    //算是 Mybatis 中核心功能了
    String resultMap = context.getStringAttribute("resultMap");
    //結果型別
    String resultType = context.getStringAttribute("resultType");
    //指令碼語言,mybatis3.2的新功能 不重要
    String lang = context.getStringAttribute("lang");
    //得到語言驅動 不重要
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    //結果集型別,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一種
    String resultSetType = context.getStringAttribute("resultSetType");
    //語句型別,STATEMENT|PREPARED|CALLABLE 的一種
    // 獲取 Statement型別 這個是需要和 JDBC做對映的
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType",StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    //獲取命令型別(select|insert|update|delete)
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache",!isSelect);
    //是否要快取select結果
    boolean useCache = context.getBooleanAttribute("useCache",isSelect);
    //僅針對巢狀結果 select 語句適用:如果為 true,就是假設包含了巢狀結果集或是分組了,這樣的話當返回一個主結果行的時候,就不會發生有對前面結果集的引用的情況。
    //這就使得在獲取巢狀的結果集的時候不至於導致記憶體不夠用。預設值:false。 
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered",false);

    // Include Fragments before parsing
    // 解析之前先解析<include>SQL片段 這個在前面 XMLMapperBuilder 
    // 中已經構建完成,這裡需要呼叫並解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration,builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //解析之前先解析<selectKey> selectKey主要涉及需要某些特殊關係來設定主鍵的值
    processSelectKeyNodes(id,parameterTypeClass,langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 做了解
    //解析成SqlSource,一般是DynamicSqlSource
    // 其實不管 Dynamic 還是 Raw 最終都會解析成 static
    SqlSource sqlSource = langDriver.createSqlSource(configuration,parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    //(僅對 insert 有用) 標記一個屬性,MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設定它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    //(僅對 insert 有用) 標記一個屬性,MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設定它的值
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId,true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    // 真正的主線在這裡
	//呼叫助手類去真正建立MappedStatement然後加入配置Configuration中
    builderAssistant.addMappedStatement(id,sqlSource,statementType,sqlCommandType,fetchSize,timeout,parameterMap,resultMap,resultTypeClass,resultSetTypeEnum,flushCache,useCache,resultOrdered,keyGenerator,keyProperty,keyColumn,langDriver,resultSets);
}
複製程式碼

這個 parseStatementNode 方法比較長,但其實你可以發現這裡無非就是對 每個 CRUD(這裡指 select delete update insert標籤) 標籤做了具體的解析,其中比較重要的就幾種,比如 ParameterType,ResultMap,解析成SqlSource(Sql的封裝),sql片段的解析。。。 其他的其實都是支線了,你可以自行去了解。

在做完屬性的一些解析後,XMLStatementBuilder 會將這些屬性再 委託 給助手物件 MapperBuilderAssistant 去進行構建 MappedStatement

// 好多引數。。
public MappedStatement addMappedStatement(
      String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap,Class<?> parameterType,String resultMap,Class<?> resultType,ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang,String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    
    //為id加上namespace字首
    // 這裡就有意思了 
    // 還記的上面說的 namespace 嗎? 
    // 這裡會使用 CRUD本身標籤的id 加上namespace構建獨一無二的id
    // 主要是因為所有mapper檔案中的 crud 標籤配置物件都是直接儲存
    // 在 configuration 中的 ,為了防止有些 標籤id會重複
    id = applyCurrentNamespace(id,false);
    // 是否是select語句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 很經典的構造者模式了,返回需要被構建的物件就可以鏈式呼叫
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration,id,sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout,statementBuilder);

    //1.引數對映 這裡很重要 
    // 因為 parameterMap 被棄用 所以這裡一般為空
    // 而真正傳入的其實應該是 parameterType 這個 Class
    setStatementParameterMap(parameterMap,parameterType,statementBuilder);
    //2.結果對映 也很重要
    setStatementResultMap(resultMap,resultType,resultSetType,statementBuilder);
    setStatementCache(isSelect,currentCache,statementBuilder);

    MappedStatement statement = statementBuilder.build();
    //建造好呼叫configuration.addMappedStatement
    // 加入configuration 這裡整個構建流程就算基本結束了。。
    configuration.addMappedStatement(statement);
    return statement;
}
複製程式碼

我們發現最終是通過 XMLMapperBuilder 的助手類去構建 MappedStatement 並傳入 Configuration 中的。我們這時候可以將上面一張流程圖更加細化一些。

引數對映和結果對映

引數對映流程

我們來看一下剛剛的還未分析完的引數對映程式碼

private void setStatementParameterMap(
      String parameterMap,Class<?> parameterTypeClass,MappedStatement.Builder statementBuilder) {
    // 給parameterMap加上namespace 但是因為parameterMap被棄用 所以一般返回null
    parameterMap = applyCurrentNamespace(parameterMap,true);

    if (parameterMap != null) {
      try {
        statementBuilder.parameterMap(configuration.getParameterMap(parameterMap));
      } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("Could not find parameter map " + parameterMap,e);
      }
    } else if (parameterTypeClass != null) {
      // 解析 parameterType生成的類物件
      List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
      // 構造ParameterMap類內部的構建類
      // 這裡主要是 parameterTypeClass 的賦值 而parameterMapping僅作為一個空列表傳入
      ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(
          configuration,statementBuilder.id() + "-Inline",parameterMappings);
      // 通過內部構建類構建ParameterMap並傳入配置物件中
      statementBuilder.parameterMap(inlineParameterMapBuilder.build());
    }
}
複製程式碼

因為 parameterMap 棄用,所以設定引數大部分是圍繞著 parameterType 走的,總結來說就是通過 parameterType 去構建一個 ParameterMap 物件(這裡是使用的ParameterMap中的內部構建者構建的)。然後將這個 ParameterMap 物件儲存在 MappedStatement 中。

其實這個 ParameterMap 物件也就三個欄位,甚至我們僅僅需要兩個。我這裡簡單寫一個 ParameterMap 類。

public class ParameterMap {
  private String id;
  private Class<?> type;
  // 其實如果 parameterMapping 棄用了這個欄位也沒什麼用了
  // 估計後面會進行重構
  private List<ParameterMapping> parameterMappings;
}
複製程式碼

官方檔案已經要刪除這個元素了。

結果對映流程

說完了引數對映,其實結果對映也大同小異。

private void setStatementResultMap(
      String resultMap,MappedStatement.Builder statementBuilder) {
    // 應用 namespace
    resultMap = applyCurrentNamespace(resultMap,true);

    List<ResultMap> resultMaps = new ArrayList<ResultMap>();
    if (resultMap != null) {
      // 進行ResultMap的解析
      // 這裡通過,分割 你可以寫成 xxxResultMap,xxxResultMap 但我還沒發現有人使用過
      String[] resultMapNames = resultMap.split(",");
      for (String resultMapName : resultMapNames) {
        try {
          // 這裡其實就是通過 resultMapName 
          // 去原來已經在 configuration解析完成的 <resultMap> 標籤
          // 配置中獲取相應的 resultMap然後加入 resultMaps中
          resultMaps.add(configuration.getResultMap(resultMapName.trim()));
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("Could not find result map " + resultMapName,e);
        }
      }
    } else if (resultType != null) {
      // resultType解析
      //<select id="selectUsers" resultType="User">
      //這種情況下,MyBatis 會在幕後自動建立一個 ResultMap,基於屬性名來對映列到 JavaBean 的屬性上。
      //如果列名沒有精確匹配,你可以在列名上使用 select 字句的別名來匹配標籤。
      //建立一個inline result map,把resultType設上就OK了,
      //然後後面被DefaultResultSetHandler.createResultObject()使用
      //DefaultResultSetHandler.getRowValue()使用
      ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
          configuration,new ArrayList<ResultMapping>(),null);
      // 最後還是封裝成了 resultMap 集合
      resultMaps.add(inlineResultMapBuilder.build());
    }
    // 將 resultMap 集合加入配置
    statementBuilder.resultMaps(resultMaps);
    // 這個直接加入配置
    statementBuilder.resultSetType(resultSetType);
}
複製程式碼

最新版本的結果對映寫在了構建流程中。

總的說來也就是 獲取 resultMap或者resultType中的值 然後通過這個值構建ResultMap傳入 MappedStatement配置中去

這個時候我們就可以畫出大概的 MappedStatement 物件的構建流程圖了。

總結

其實整個 Mybatis 初始化流程就是 對配置檔案進行解析成配置物件裝入記憶體以便在後面執行的過程中使用。而 mybatis 的配置檔案會儲存到對應的 Configuration 物件中,而對映配置檔案會專門解析 CRUD 標籤存入 MappedStatement 物件中,最終這個 MappedStatement 物件也會加入集合並存入到 Configuration 中去。這其中主要用到了 工廠模式構建者模式

Mybatis主要有兩個執行模組,第一就是這篇文章我們所講的 構建部分,還有一部分就是 Sql的執行部分,關於執行部分我會在下一篇文章中分享。