Myabtis原始碼分析五-Mybatis配置載入完全圖解,建造者模式的使用

語言: CN / TW / HK

​一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第10天,[點選檢視活動詳情]

一、Mybatis執行流程概述

為了熟悉Mybatis的執行流程,我們先看一段程式碼

``` public class MybatisDemo {

private SqlSessionFactory sqlSessionFactory;

@Before
public void init() throws IOException {
    //--------------------第一步:載入配置---------------------------
    // 1.讀取mybatis配置檔案創SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 1.讀取mybatis配置檔案創SqlSessionFactory
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    inputStream.close();
}

@Test
// 快速入門
public void quickStart() throws IOException {
    //--------------------第二部,建立代理物件---------------------------
    // 2.獲取sqlSession   
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 3.獲取對應mapper
    TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);

    //--------------------第三步:獲取資料---------------------------
    // 4.執行查詢語句並返回單條資料
    TUser user = mapper.selectByPrimaryKey(2);
    System.out.println(user);

    System.out.println("----------------------------------");

    // 5.執行查詢語句並返回多條資料

// List users = mapper.selectAll(); // for (TUser tUser : users) { // System.out.println(tUser); // } } } ```

以上是我們一個使用mybatis訪問資料的demo,通過對快速入門程式碼的分析,可以把 MyBatis 的執行流程分為三大階段:

  1. 初始化階段:讀取 XML 配置檔案和註解中的配置資訊,建立配置物件,並完成各個模組的初始化的工作;
  2. 代理封裝階段:封裝 iBatis 的程式設計模型,使用 mapper 介面開發的初始化工作;
  3. 資料訪問階段:通過 SqlSession 完成 SQL 的解析,引數的對映、SQL 的執行、結果的解析過程;

今天我們就介紹以下第一個階段中,Mybatis是如何讀取配置的

二、配置載入的核心類

2.1 建造器三個核心類

在 MyBatis 中負責載入配置檔案的核心類有三個,類圖如下:

  • BaseBuilder:所有解析器的父類,包含配置檔案例項,為解析檔案提供的一些通用的方法;
  • XMLConfigBuilder: 主要負責解析 mybatis-config.xml;
  • XMLMapperBuilder: 主要負責解析對映配置 Mapper.xml 檔案;
  • XMLStatementBuilder: 主要負責解析對映配置檔案中的 SQL 節點;

XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder 這三個類在配置檔案載入過程中非常重要,具體分工如下圖所示:

這三個類使用了建造者模式對 configuration 物件進行初始化,但是沒有使用建造者模式\ 的“肉體”(流式程式設計風格),只用了靈魂(遮蔽複雜物件的建立過程),把建造者模式演繹\ 成了工廠模式;後面還會對這三個類原始碼進行分析;

居然這三個物件使用的是建造者模式,那麼我們稍後介紹下什麼是建造者模式

三、建造者模式

3.1 什麼是建造者模式

建造者模式(BuilderPattern)使用多個簡單的物件一步一步構建成一個複雜的物件。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。

建造者模式類圖如下:

各要素如下:

  • Product:要建立的複雜物件
  • Builder:給出一個抽象介面,以規範產品物件的各個組成成分的建造。這個介面規定要實現複雜物件的哪些部分的建立,並不涉及具體的物件部件的建立;
  • ConcreteBuilder:實現 Builder 介面,針對不同的商業邏輯,具體化複雜物件的各部分的建立。 在建造過程完成後,提供產品的例項;
  • Director:呼叫具體建造者來建立複雜物件的各個部分,在指導者中不涉及具體產品的資訊,只負責保證物件各部分完整建立或按某種順序建立;

應用舉例:紅包的建立是個複雜的過程,可以使用構建者模式進行建立

程式碼示例:

1、紅包物件RedPacket 

```

public class RedPacket {

private String publisherName; //發包人

private String acceptName; //收包人

private BigDecimal packetAmount; //紅包金額

private int packetType; //紅包型別

private Date pulishPacketTime; //發包時間

private Date openPacketTime; //搶包時間

public RedPacket(String publisherName, String acceptName, BigDecimal packetAmount, int packetType, Date pulishPacketTime, Date openPacketTime) {
    this.publisherName = publisherName;
    this.acceptName = acceptName;
    this.packetAmount = packetAmount;
    this.packetType = packetType;
    this.pulishPacketTime = pulishPacketTime;
    this.openPacketTime = openPacketTime;
}

public String getPublisherName() {
    return publisherName;
}

public void setPublisherName(String publisherName) {
    this.publisherName = publisherName;
}

public String getAcceptName() {
    return acceptName;
}

public void setAcceptName(String acceptName) {
    this.acceptName = acceptName;
}

public BigDecimal getPacketAmount() {
    return packetAmount;
}

public void setPacketAmount(BigDecimal packetAmount) {
    this.packetAmount = packetAmount;
}

public int getPacketType() {
    return packetType;
}

public void setPacketType(int packetType) {
    this.packetType = packetType;
}

public Date getPulishPacketTime() {
    return pulishPacketTime;
}

public void setPulishPacketTime(Date pulishPacketTime) {
    this.pulishPacketTime = pulishPacketTime;
}

public Date getOpenPacketTime() {
    return openPacketTime;
}

public void setOpenPacketTime(Date openPacketTime) {
    this.openPacketTime = openPacketTime;
}

@Override
public String toString() {
    return "RedPacket [publisherName=" + publisherName + ", acceptName="
            + acceptName + ", packetAmount=" + packetAmount
            + ", packetType=" + packetType + ", pulishPacketTime="
            + pulishPacketTime + ", openPacketTime=" + openPacketTime + "]";
}

} ```

2、構建物件

``` public class Director {

public static void main(String[] args) {
    RedPacket redPacket = RedPacketBuilderImpl.getBulider().setPublisherName("DK")
                                                           .setAcceptName("粉絲")
                                                           .setPacketAmount(new BigDecimal("888"))
                                                           .setPacketType(1)
                                                           .setOpenPacketTime(new Date())
                                                           .setPulishPacketTime(new Date()).build();

    System.out.println(redPacket);
}

} ```

PS:流式程式設計風格越來越流行,如 zookeeper 的 Curator、JDK8 的流式程式設計等等都是例子。流式程式設計的優點在於程式碼程式設計性更高、可讀性更好,缺點在於對程式設計師編碼要求更高、不太利於除錯。建造者模式是實現流式程式設計風格的一種方式;

3.2 與工廠模式區別

建造者模式應用場景如下:

  • 需要生成的物件具有複雜的內部結構,例項化物件時要遮蔽掉物件程式碼與複雜物件的例項化過程解耦,可以使用建造者模式;簡而言之,如果“遇到多個構造器引數時要考慮用構建器”;
  • 物件的例項化是依賴各個元件的產生以及裝配順序,關注的是一步一步地組裝出目標對
  • 象,可以使用建造器模式;

建造者模式與工程模式的區別在於:

| | | | | | -------- | -------- | ------------------------------------------------- | ----------------------------------------------- | | 設計模式 | 形象比喻 | 物件複雜度 | 客戶端參與程度 | | 工廠模式 | 生產大眾版 | 關注的是一個產品整體,無須關心產品的各部分是如何創建出來的; | 客戶端對產品的建立過程參與度低,物件例項化時屬性值相對比較固定; | | 建造者模式 | 生產定製版 | 建造的物件更加複雜,是一個複合產品,它由各個部件複合而成,部件不同產品物件不同,生成的產品粒度細; | 客戶端參與了產品的建立,決定了產品的型別和內容,參與度高;適合例項化物件時屬性變化頻繁的場景; |

四、Configuration 物件介紹

例項化並初始化 Configuration 物件是第一個階段的最終目的,所以熟悉 configuration 對\ 象是理解第一個階段程式碼的核心;configuration 物件的關鍵屬性解析如下:

  • MapperRegistry:mapper 介面動態代理工廠類的註冊中心。在 MyBatis 中,通過mapperProxy 實現 InvocationHandler 介面,MapperProxyFactory 用於生成動態代理的例項物件;
  • ResultMap:用於解析 mapper.xml 檔案中的 resultMap 節點,使用 ResultMapping 來封裝id,result 等子元素;
  • MappedStatement:用於儲存 mapper.xml 檔案中的 select、insert、update 和 delete 節點,同時還包含了這些節點的很多重要屬性;
  • SqlSource:用於建立 BoundSql,mapper.xml 檔案中的 sql 語句會被解析成 BoundSql 物件,經過解析 BoundSql 包含的語句最終僅僅包含?佔位符,可以直接提交給資料庫執行;

Configuration物件圖解:

需要特別注意的是 Configuration 物件在 MyBatis 中是單例的,生命週期是應用級的,換句話說只要 MyBatis 執行 Configuration 物件就會獨一無二的存在;在 MyBatis 中僅在\ org.apache.ibatis.builder.xml.XMLConfigBuilder.XMLConfigBuilder(XPathParser, String, Properties)中有例項化 configuration 物件的程式碼,如下圖:

Configuration 物件的初始化(屬性複製),是在建造 SqlSessionfactory 的過程中進行的,接下\ 來分析第一個階段的內部流程;

五、配置載入流程解析

5.1 配置載入過程

可以把第一個階段配置載入過程分解為四個步驟,四個步驟如下圖:

​\ 第一步:通過 SqlSessionFactoryBuilder 建造 SqlSessionFactory,並建立 XMLConfigBuilder 對\ 象 讀 取 MyBatis 核 心 配 置 文 件 , 見 原始碼方 法 :\ org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader, String, Properties):

public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { //讀取配置檔案 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse());//解析配置檔案得到configuration物件,並返回SqlSessionFactory } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }

第二步:進入 XMLConfigBuilder 的 parseConfiguration 方法,對 MyBatis 核心配置檔案的各個\ 元素進行解析,讀取元素資訊後填充到 configuration 物件。在 XMLConfigBuilder 的\ mapperElement()方法中通過 XMLMapperBuilder 讀取所有 mapper.xml 檔案;見方法:\ org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XNode);

``` public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }

private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析節點 propertiesElement(root.evalNode("properties")); //解析節點 Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); //解析節點 typeAliasesElement(root.evalNode("typeAliases")); //解析節點 pluginElement(root.evalNode("plugins")); //解析節點 objectFactoryElement(root.evalNode("objectFactory")); //解析節點 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //解析節點 reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings);//將settings填充到configuration // read it after objectFactory and objectWrapperFactory issue #631 //解析節點 environmentsElement(root.evalNode("environments")); //解析節點 databaseIdProviderElement(root.evalNode("databaseIdProvider")); //解析節點 typeHandlerElement(root.evalNode("typeHandlers")); //解析節點 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } ```

第三步:XMLMapperBuilder 的核心方法為 configurationElement(XNode),該方法對 mapper.xml 配置檔案的各個元素進行解析,讀取元素資訊後填充到 configuration 物件。

private void configurationElement(XNode context) { try { //獲取mapper節點的namespace屬性 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } //設定builderAssistant的namespace屬性 builderAssistant.setCurrentNamespace(namespace); //解析cache-ref節點 cacheRefElement(context.evalNode("cache-ref")); //重點分析 :解析cache節點----------------1------------------- cacheElement(context.evalNode("cache")); //解析parameterMap節點(已廢棄) parameterMapElement(context.evalNodes("/mapper/parameterMap")); //重點分析 :解析resultMap節點----------------2------------------- resultMapElements(context.evalNodes("/mapper/resultMap")); //解析sql節點 sqlElement(context.evalNodes("/mapper/sql")); //重點分析 :解析select、insert、update、delete節點 ----------------3------------------- 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); } }

在 XMLMapperBuilder 解析過程中,有四個點需要注意:

  1. resultMapElements(List)方法用於解析 resultMap 節點,這個方法非常重要, 一定要跟原始碼理解;解析完之後資料儲存在 configuration 物件的 resultMaps 屬性中;如下圖
  2. 2XMLMapperBuilder 中在例項化二級快取(見 cacheElement(XNode))、例項化 resultMap (見 resultMapElements(List))過程中都使用了建造者模式,而且是建造者模 式的典型應用;
  3. XMLMapperBuilder 和 XMLMapperStatmentBuilder 有 自 己 的 “ 祕 書 ” MapperBuilderAssistant。XMLMapperBuilder 和 XMLMapperStatmentBuilder 負責解析 讀取配置檔案裡面的資訊,MapperBuilderAssistant 負責將資訊填充到 configuration。 將檔案解析和資料的填充的工作分離在不同的類中,符合單一職責原則;
  4. 在 buildStatementFromContext(List)方法中,建立 XMLStatmentBuilder 解析 Mapper.xml 中 select、insert、update、delete 節點

第四步:在 XMLStatmentBuilder 的 parseStatementNode()方法中,對 Mapper.xml 中 select、 insert、update、delete 節點進行解析,並呼叫 MapperBuilderAssistant 負責將資訊填充到 configuration。在理解 parseStatementNod()方法之前,有必要了解 MappedStatement,這個 類 用 於 封 裝 select 、 insert 、 update 、 delete 節 點 的 信 息 ; 如 下 圖 所 示 :

至此,整個Mybatis的配置即載入完畢,整個載入流程圖如下:

\