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的配置即加載完畢,整個加載流程圖如下:

\