1. 程式人生 > 資料庫 >Mybatis3詳解(十六)——Mybatis執行原理之SqlSessionFactory的構建過程

Mybatis3詳解(十六)——Mybatis執行原理之SqlSessionFactory的構建過程

1、寫在前面

       前面的一系列文章已經詳細的介紹了Mybatis的各種使用方法,所以這章我們來更加深入的瞭解Mybatis,講述一下Mybatis的內部解析與執行原理,但是這章所講的只涉及基本的框架和核心程式碼,並不會面面俱到,所以本章中的一些細節將會被忽略掉,需要仔細研究的可以自行查閱相關書籍或者問度娘。雖然這章不可能讓你對Mybatis的所有知識點都瞭解,但是當我們掌握了Mybatis的執行原理,就可以知道Mybatis是怎麼執行的,也為後面大家閱讀Mybatis原始碼奠定一點基礎吧。

       Mybatis的執行分為兩大部分:

  1. 一是SqlSessionFactory的建立過程,它主要是通過XMLConfigBuilder將我們的配置檔案讀取並且快取到Configuration物件中,然後通過Configuration來建立SqlSessionFactory物件。
  2. 二是SqlSession的執行過程,這個過程是Mybatis中最複雜的,它包含了許多複雜的技術,包括反射技術和動態代理技術等,這是Mybatis底層架構的基礎。


       MyBatis的主要成員元件(成員):

       在第一章的時候,簡單的介紹了Mybatis的有哪些元件(成員),這裡再次詳細的介紹一下:

  1. SqlSessionFactoryBuilder:會根據XML配置或是Java配置來生成SqlSessionFactory物件。採用建造者模式(簡單來說就是分步構建一個大的物件,例如建造一個大房子,採用購買磚頭、砌磚、粉刷牆面的步驟建造,其中的大房子就是大物件,一系列的建造步驟就是分步構建)。
  2. SqlSessionFactory:用於生成SqlSession,可以通過 SqlSessionFactory.openSession() 方法建立 SqlSession 物件。使用工廠模式(簡單來說就是我們獲取物件是通過一個類,由這個類去建立我們所需的例項並返回,而不是我們自己通過new去建立)。
  3. Configuration:MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中。
  4. SqlSession:相當於JDBC中的 Connection物件,可以用 SqlSession 例項來直接執行被對映的 SQL 語句,也可以獲取對應的Mapper。
  5. Executor:MyBatis 中所有的 Mapper 語句的執行都是通過 Executor 執行的,負責SQL語句的生成和查詢快取的維護 。
  6. StatementHandler:封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等。
  7. ParameterHandler:負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別。
  8. ResultSetHandler:負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合。
  9. TypeHandler:負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換。
  10. MappedStatement:作用是儲存一個對映器節點<select|update|delete|insert>中的內容。MappedStatement封裝了Statement的相關資訊,包括我們配置的SQL、SQL的id、快取資訊、resultMap、ParameterType、resultType、resultMap等重要配置內容等。Mybatis可以通過它來獲取某條SQL配置的所有資訊。它還有一個非常重要的屬性是SqlSource。
  11. SqlSource:負責提供BoundSql物件的地方。作用就是根據上下文和引數解析生成真正的SQL,然後將資訊封裝到BoundSql物件中,並返回。我們在Mapper對映檔案中定義的SQL,這個SQL可以有佔位符和一系列引數的(如select * from t_user where id = #{id}),也可以是動態SQL的形式,這裡的SqlSource就是用來將它解析為真正的SQL(如:select * from t_user where id = ?)。注意:SqlSource是一個介面,而不是一個實現類。對它而言有這麼幾個重要的實現類:DynamicSQLSource、ProviderSQLSource、RawSQLSource、StaticSQLSource。例如前面動態SQL就採用了DynamicSQLSource配合引數解析解析後得到的。它算是起到生成真正SQL語句的一箇中轉站吧。
  12. BoundSql:它是一個結果物件,它是通過SqlSource來獲取的。作用是通過SqlSource對對映檔案的SQL和引數聯合解析得到的真正SQL和引數。什麼意思呢?就是BoundSql包含了真正的SQL語句(由SqlSource生成的,如select * from t_user where id = ?),而且還包含了SQL語句增刪改查的引數,而SqlSource是負責將對映檔案中定義的SQL生成真正的SQL語句(算是對映檔案中的SQL生成真正的SQL語句的中轉站),這裡搞得我有點昏 imageimageimageBoundSql有3個常用的屬性:sql、parameterObject、parameterMappings,這裡就不做討論了,通過名字應該很容易理解它的用處。

       以上主要元件(成員)在一次資料庫操作中基本都會涉及。

       注:圖片來自《》

20141028140852531

2、SqlSessionFactory的構建過程

       SqlSessionFactory 是MyBatis的核心類之一, 其最重要的功能就是提供建立MyBatis的核心介面SqlSession,所以我們要先建立SqlSessionFactory,它是通過Builder(建造者)模式來建立的,所以在Mybatis中提供了SqlSessionFactoryBuilder類。其構建分為兩步。

  1. 第 1 步: 通過 org.apache.ibatis.builder.xml.XMLConfigBuilder 解析配置的XML檔案,讀出所配置的引數,並將讀取的內容存入org.apache.ibatis.session.Configuration類物件中。而Configuration採用的是單例模式,幾乎所有的 MyBatis 配置內容都會存放在這個單例物件中,以便後續將這些內容讀出。
  2. 第2步:使用Confinguration物件去建立SqlSessionFactory。MyBatis 中的 SqlSessionFactory 是一個介面,而不是一個實現類,為此MyBatis提供了一個預設的實現類org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。在大部分情況下都沒有必要自己去建立新的SqlSessionFactory 實現類,而是由系統建立。

       這種建立的方式就是一種 Builder 模式,對於複雜的物件而言,使用構造引數很難實現。這時使用一個類(比如 Configuration)作為統領,一步步地構建所需的內容,然後通過它去建立最終的物件(比如 SqlSessionFactory),這樣每一步都會很清晰,這種方式值得大家學習,並且在工作中使用。

       下面我們就來學習一下SqlSessionFactory是如何構建的。程式入口程式碼如下:

	//1、載入 mybatis 全域性配置檔案
	InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
	//InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
	//2、建立SqlSessionFactory物件
	SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
	//3、根據 sqlSessionFactory sqlSession
	SqlSession sqlSession = sqlSessionFactory.openSession();
	//4、建立Mapper介面的的代理物件,getMapper方法底層會通過動態代理生成UserMapper的代理實現類
	UserMapper mapper = sqlSession.getMapper(UserMapper.class);


       ①、首先會執行SqlSessionFactoryBuilder類中的build(InputStream inputStream)方法。

	//最初呼叫SqlSessionFactoryBuilder類中的build
	public SqlSessionFactory build(InputStream inputStream) {
		//然後呼叫了過載方法
		return build(inputStream, null, null);
	}

       ②、上面的方法中呼叫了另一個過載的build方法。

    //呼叫的過載方法
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //XMLConfigBuilder是專門解析mybatis的配置檔案的類
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //又呼叫了一個過載方法。parser.parse()的返回值是Configuration物件,這是解析配置檔案最核心的方法,非常重要!
      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.
      }
    }
  }

       可以發現其內部定義了一個XMLConfigBuilder物件,然後通過這個物件呼叫自身的parse()方法對配置檔案進行解析,這個parse()方法的返回值為Configuration物件,最後將返回的Configuration物件作為引數呼叫build()方法,從而完成SqlSessionFactory的建立。所以我們這裡需要注意的就是這兩行程式碼:XMLConfigBuilder物件和呼叫build(parser.parse())方法返回SqlSessionFactory。

       (1)、XMLConfigBuilder從類名就可以看出,這是用來解析XML配置檔案的類,其父類為BaseBuilder。我們來看一下這個類構造方法:

image

       通過檢視XMLConfigBuilder中構造方法的原始碼,可以得知XML配置檔案最終是由org.apache.ibatis.parsing.XPathParser封裝的XPath解析的。第一個構造方法通過XPathParser構造方法傳入我們讀取的XML流檔案、Properites流檔案和environment等引數得到了一個XpathParser例項物件parser,這裡parser已包含全域性XML配置檔案解析後的所有資訊,然後再將parser作為引數傳給XMLConfigBuilder構造方法。其中XMLConfigBuilder 構造方法還呼叫了父類BaseBuilder的構造方法BaseBuilder(Configuration),這裡傳入了一個Configuration物件,用來初始化Configuration物件,我們來繼續進入看一下:

image

       注意:這裡的重點是建立了一個Configuration 物件,並且完成了初始化,這個Configuration是用來封裝所有配置檔案的類,所以非常非常重要!!!同時還初始化了別名和型別處理器,所以我們預設可以使用這些特性。額外這個父類BaseBuilder還包含了MapperBuilderAssistant, SqlSourceBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLScriptBuilder, XMLStatementBuilder等子類,這些子類都是用來解析MyBatis各個配置檔案,他們通過BaseBuilder父類共同維護一個全域性的Configuration物件。只是XMLConfigBuilder的作用就是解析全域性配置檔案,呼叫BaseBuilder其他子類解析其他配置檔案,生成最終的Configuration物件。


       (2)、然後我們重點來看一下parse()方法,這是最核心的方法。進入parse.parse()方法:

  public Configuration parse() {
    //用於標識XMLConfigBuilder
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //用於解析MyBatis全域性配置檔案<configuraction>標籤中的相關配置
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

       注意:XMLConfigBuilder的 parsed 屬性,預設值是false(上面的構造方法中可以看到),它是用來標識XMLConfigBuilder物件的。當建立了一個XMLConfigBuilder物件,並進行解析配置檔案的時候,parsed的值就變成了true。如果第二次進行解析的時候就會丟擲BuilderException異常,提示每個XMLConfigBuilder只能使用一次,從而確保了Configuration物件是單例的。因為Configuration物件是通過XMLConfigBuilder的parse()去解析的。

       Configuration物件的具體解析是通過parseConfiguration(XNode root)方法來完成的。這個方法用於解析MyBatis 全域性配置檔案與SQL 對映檔案中的相關配置,引數中"/configuration" 就是對應全域性配置檔案中的<configuration> 標籤,parser 是XPathParser 類的例項(前面已經介紹過了),通過該物件解析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"));
      // 解析<mappers>標籤中的資訊
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

       以上操作會把MyBatis 全域性配置檔案與SQL 對映檔案中每一個節點的資訊都讀取出來,然後儲存在Configuration單例中,Configuration分別對以下內容做出了初始化:properties 屬性 ;typeAliases 類型別名;plugins 外掛;objectFactory 物件工廠;settings 設定;environments 環境;databaseIdProvider 資料庫廠商標識;typeHandlers 型別處理器;mappers 對映器等。這其中還涉及到了很多的方法,在這裡就不11講述了,大家可以自己進行檢視。這裡主要來看一下mappers對映器,因為我們需要頻繁的訪問它,因此它算是這裡最重要的內容了吧。進入mapperElement(XNode parent):

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
	  // 遍歷所有mappers節點下的所有元素
      for (XNode child : parent.getChildren()) {
		// 如是package引入的方式
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
		// 如果是mapper引入的方式
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
		  // 如果是resource
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
			// 如果是url
          } 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();
			// 如果是mapperClass
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
			// 新增至Configuration物件中
            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子節點,是以包名引入對映器,則將該包下所有Class註冊到Configuration的mapperRegistry中。
  • 如果遍歷到mapper子節點的class屬性,是以class的方式引入對映器,則將制定的Class註冊到註冊到Configuration的mapperRegistry中。
  • 如果遍歷到mapper子節點的resource或者url屬性,是通過resource或者url方式引入對映器,則直接對資原始檔進行解析:

        所以在通過resource或者url方式引入對映器的程式碼中,可以注意到定義了一個XMLMapperBuilder 類,然後呼叫了parse()方法,這目的就很明顯了,如果遍歷到是以mapper子節點的resource或者url屬性方式引入的對映器,那麼所有的Sql對映檔案都是用XMLMapperBuilder 類的parse()方法來進行解析。

       注意:其實到這裡就已經可以不用往下看了,如果你想更加深入瞭解一下,那就繼續往下滑吧!!!

       我們先分別來看一下resource或者url屬性方式引入對映器的構造方法(它兩共用一個):

image

       XPathParser將mapper配置檔案解析成Document物件後封裝到一個XPathParser物件,再將XPathParser物件作為引數傳給XMLMapperBuilder構造方法並構造出一個XMLMapperBuilder物件,XMLMapperBuilder物件的builderAssistant欄位是一個MapperBuilderAssistant物件,同樣也是BaseBuilder的一個子類,其作用是對MappedStatement物件進行封裝。

       有了XMLMapperBuilder物件後,就可以進入解析mapper對映檔案的過程,進入parse()方法:

image

       呼叫XMLMapperBuilder的configurationElement方法,對mapper對映檔案進行解析

image

       mapper對映檔案必須有namespace屬性值,否則丟擲異常,將namespace屬性儲存到XMLMapperBuilder的MapperBuilderAssistant物件中,以便其他方法呼叫。
       該方法對mapper對映檔案每個標籤逐一解析並儲存到Configuration和MapperBuilderAssistant物件中,最後呼叫buildStatementFromContext方法解析select、insert、update和delete節點。

image

       buildStatementFromContext方法中呼叫XMLStatementBuilder的parseStatementNode()方法來完成解析。

image

image

       注意:解析所有的Sql語句會封裝一個MappedStatement中,MappedStatement中包含了許多我們配置的SQL、SQL的id、快取資訊、resultMap、ParameterType、resultType、resultMap等重要配置內容。最重要的是它還有一個屬性sqlSource。

       通過上面的方法可以看到SQL語句封裝到一個SqlSource物件,SqlSource是個介面,如果是動態SQL就建立DynamicSqlSource實現類,否則建立StaticSqlSource實現類。

image

       SqlSource是MappedStatement的一個屬性,它只是一個介面。它的主要作用是根據上下文和引數解析生成需要的Sql。

       SqlSource介面中有一個getBoundSql方法,這個方法就是用來獲取BoundSql的:

image

       SqlSource介面中還有如下這幾個重要的實現類:

image


       BoundSql是一個結果集物件,也就是SqlSource通過對對映檔案的SQL和引數解析得到的真正的SQL和引數。

       注:MappedStatement、SqlSource和BoundSql在最上面已經詳細的介紹了,自行滑到上面檢視。



       ③、Mybatis的配置檔案解析完成後,會將資訊儲存在Configuration物件中,之後通過XMLConfigBuilder類中的parse()方法返回,然後再將Configuration物件作為引數傳遞到build(Configuraction config)方法。進入這個build方法:

// SqlSessionFactoryBuilder另一個build方法
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

       可以看到最後接著返回一個DefaultSqlSessionFactory物件,DefaultSqlSessionFactory就是SqlSessionFactory的一個實現類,到這裡SqlSessionFactory物件就完成了建立的全部過程。

       SqlSessionFactory構建過程中的時序圖:

image

       參考連結:

  1. 《Java EE 網際網路輕量級框架整合開發》