天天看點

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

作者:Java機械師

一、Mybatis運作流程概述

為了熟悉Mybatis的運作流程,我們先看一段代碼

java複制代碼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<TUser> 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 中負責加載配置檔案的核心類有三個,類圖如下:

Mybatis配置加載流程完全圖解,建造者模式的優雅應用
  • BaseBuilder:所有解析器的父類,包含配置檔案執行個體,為解析檔案提供的一些通用的方法;
  • XMLConfigBuilder: 主要負責解析 mybatis-config.xml;
  • XMLMapperBuilder: 主要負責解析映射配置 Mapper.xml 檔案;
  • XMLStatementBuilder: 主要負責解析映射配置檔案中的 SQL 節點;

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

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

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

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

三、建造者模式

3.1 什麼是建造者模式

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

建造者模式類圖如下:

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

各要素如下:

  • Product:要建立的複雜對象
  • Builder:給出一個抽象接口,以規範産品對象的各個組成成分的建造。這個接口規定要實作複雜對象的哪些部分的建立,并不涉及具體的對象部件的建立;
  • ConcreteBuilder:實作 Builder 接口,針對不同的商業邏輯,具體化複雜對象的各部分的建立。 在建造過程完成後,提供産品的執行個體;
  • Director:調用具體建造者來建立複雜對象的各個部分,在指導者中不涉及具體産品的資訊,隻負責保證對象各部分完整建立或按某種順序建立;

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

代碼示例:

1、紅包對象RedPacket

typescript複制代碼
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、建構對象

typescript複制代碼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對象圖解:

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

需要特别注意的是 Configuration 對象在 MyBatis 中是單例的,生命周期是應用級的,換句話說隻要 MyBatis 運作 Configuration 對象就會獨一無二的存在;在 MyBatis 中僅在 org.apache.ibatis.builder.xml.XMLConfigBuilder.XMLConfigBuilder(XPathParser, String, Properties)中有執行個體化 configuration 對象的代碼,如下圖:

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

Configuration 對象的初始化(屬性複制),是在建造 SqlSessionfactory 的過程中進行的,接下 來分析第一個階段的内部流程;

五、配置加載流程解析

5.1 配置加載過程

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

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

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

typescript複制代碼  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);

scss複制代碼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
     //解析<properties>節點
      propertiesElement(root.evalNode("properties"));
      //解析<settings>節點
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      //解析<typeAliases>節點
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析<plugins>節點
      pluginElement(root.evalNode("plugins"));
      //解析<objectFactory>節點
      objectFactoryElement(root.evalNode("objectFactory"));
      //解析<objectWrapperFactory>節點
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析<reflectorFactory>節點
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);//将settings填充到configuration
      // read it after objectFactory and objectWrapperFactory issue #631
      //解析<environments>節點
      environmentsElement(root.evalNode("environments"));
      //解析<databaseIdProvider>節點
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //解析<typeHandlers>節點
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析<mappers>節點
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
           

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

csharp複制代碼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配置加載流程完全圖解,建造者模式的優雅應用

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

Mybatis配置加載流程完全圖解,建造者模式的優雅應用

繼續閱讀