1. 程式人生 > >Mybatis的解析和基本執行原理

Mybatis的解析和基本執行原理

Mybatis執行過程

Mybatis的執行過程分為兩大步:第1步,讀取配置檔案快取到Configuration物件,用於建立SqlSessionFactory;第2步,SqlSession的執行過程。相對而言,SqlSessionFactory的建立還算比較容易理解,而SqlSession的執行過程就不那麼簡單了,它包括許多複雜的技術,要先掌握反射技術和動態代理,這裡主要用到的是JDK動態代理,不熟悉的話請點選這裡瞭解一下

構建SqlSessionFactory過程

SqlSessionFactory是Mybatis的核心類之一,其最重要的功能就是提供建立MyBatis的核心介面SqlSession,所以首先建立SqlSessionFactory,為此要提供配置檔案和相關的引數。MyBatis是一個複雜的系統,它採用了Builder模式去建立SqlSessionFactory,在實際中課通過SqlSessionFacyoryBuilder去建立,構建分為兩步:

 1. 通過org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML檔案讀取所配置的引數,
   並將讀取的內容存入Configuration類物件中。而Configuration採用的是單例模式,幾乎所有的MyBatis
   配置內容都會存放在這個單例物件中

 2. 使用Configuration物件去建立SqlSessionFactory。MyBatis中的SqlSessionFactory是一個介面,
   而不是一個實現類,為此MyBatis提供了一個預設的實現類DefaultSqlSessionFactory.在大部分情況下
   沒有必要自己去建立新的實現類。
     這種建立的方式就是一種Builder模式,對於複雜的物件而言,使用構造器引數很難實現,這是使用一個類(比如Configuration)
   作為統領,一步步構建所需的內容,然後通過它去建立最終的物件(比如SqlSessionFactory),這樣每一步都會很清晰

XMLConfigBuilder解析Xml的原始碼:

 private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode
("properties")); //issue #117 read properties first typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); settingsElement(root.evalNode
("settings")); environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 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); } } 從原始碼中可以看到,他是通過一步步解析XML內容得到對應的資訊,而這些資訊正是我們在配置檔案中配置的內容, 這裡只討論typeHandlers解析的方法,其他的類似 配置的typeHandler都會被註冊到typeHandlerRegister物件中去,它的定義是放在XMLConfiguration的父類 BaseBuilder中的,程式碼如下: protected final TypeHandlerRegistry typeHandlerRegistry; public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } 可以看出typeHandlerRegistry是Configuration單例的一個屬性,所以可以通過COnfiguration單例拿到typeHandlerRegistry, 從而進行註冊typeHandler

構建Configuration

在SqlSessionFactory構建中,Configuration是最重要的,它的作用是:

  • 讀入配置檔案,包括基礎配置的Xml和對映器XML(或註解)
  • 初始化一些基礎配置,比如Mybatis別名等,一些重要的類物件(如外掛、對映器Object工廠等)
  • 提供單例,為後續的建立SessionFactory服務提供配置的引數
    顯然Configuration不會是一個簡單的類,MyBatis的配置資訊都來自於此,首先他會讀出XML配置的資訊,然後把它們儲存在Configuration單例中,它會做如下初始化:
  • properties全域性引數
  • typeAliases別名
  • Plugins 外掛
  • objectFactory 物件工廠
  • reflectionFactory 反射工廠
  • settings 環境設定
  • environment 資料庫環境
  • databaseIdProvider 資料庫標識
  • Mappers 對映器

    他們都會以類似typeHandler註冊那樣的方法被存放到Configuration單例,以便將來取出

構建對映器的內部組成

當XMLConfiguration解析XML是會將每一個SQL和其配置的內容儲存起來。一般而言,在MyBatis中一條SQL和它相關的配置是有3個部分組成的,它們分別是MappedStatement、SqlSource和BoundSql。

  • MappedStatement的作用儲存一個對映器節點(select | insert | delete | update)的內容。它是一個類,包括許多我們配置的SQL、SQL的id、快取資訊、resultMap、parameterType、resultType等重要的配置資訊,它還有一個重要的屬性sqlSource。MyB通過它來獲取某條SQL配置的所有資訊。
  • SqlSource 是提供BoundSql物件的地方,它是MappedStatement的一個重要屬性。注意,它是一個介面,而不是一個實現類。它有幾個重要的實現類:DynamicSqlSource、ProviderSqlSource、RawSqlSource、StaticSqlSource。它的作用是根據上下文和引數解析生成需要的SQL,這個介面只定義了一個介面方法—–getBoundSql(parameterObject),使用它就可以得到一個BoundSql物件。
  • BoundSql是一個結果物件,也就是SqlSource通過對SQL和引數的解析得到的SQL和引數,它是建立SQL和引數的地方,他有常用的3個屬性:sql、parameterObject、parameterMappings
  • 對映器內部組成圖:
    組成圖
    這裡只是列舉了主要的方法和屬性。MappedStatement物件涉及的東西比較多,一般不去修改它,因為容易產生不必要的錯誤。SqlSource是一個介面,它的這個主要作用是根據引數和其他的規則組裝SQL,這些都是很複雜的東西,好在MyBatis本身已經實現了它,一般不需要去修改。對於最終的引數和SQL都反映在BoundSql類物件上,在外掛中往往需要拿到它進而拿到當前執行的SQL和引數,從而對執行過程做出必要的修改,來滿足特殊的需求,這裡的討論對MyBatis外掛的開發是至關重要的
由上圖可知BoundSql會提供3個主要的屬性:parameterMappings,parameterObject和sql
  • parameterObject為引數本身,可以傳遞簡單物件、POJO、Map或@Param註解的引數,由於它在外掛中相當有用,有必要討論它的一些規則
    ①.傳遞簡單物件,包括int、String、float、double等。當傳遞int型別時,MyBatis會把引數變為Integer物件傳遞,類似的long、String、float、double也是如此。
    ②.傳遞POJO或者Map,parameterObject就是傳入的POJO或者Map
    ③.傳遞多個引數,如果沒有使用@Param註解,那麼MyBatis會把parameterObject變成一個Map< String,Object>物件,其鍵值的關係是按照順序來規劃的,類似於{“1”:p1, “2”:p2, “3”:p3……,”param1”:p1, “param2”:p2, “param3”:p3…}這樣的形式。所以在編寫的時候可以使用#{param1}或者#{1}去引用第一個引數。
    ④.使用@Param註解,MyBatis就會把parameterObject也變成一個Map< String,Object>物件,類似於沒有@Param註解,只是把其數字的鍵值置換成@Param註解鍵值。比如註解@Param(“key1”) String p1、@Param(“key2”) int p2、@Param(“key3”) User p3,那麼parameterObject物件就是一個Map物件,它的鍵值包含{“key1”:p1,”key2”:p2, “key3”:p3,”param1”:p1, “param2”:p2,”param3”:p3}

  • parameterMappings是一個List,它的每一個元素都是ParameterMapping物件。物件會描述引數,引數包括屬性名稱、表示式、javaType、jdbcType、typeHandler等重要資訊,一般不需要去改變他,通過它就可以實現引數和SQL的結合,以便於PreparedStatement能夠通過它找到paramterObject物件的屬性設定引數,使得程式能準確執行。

  • sql屬性就是書寫在對映器裡面的一條被SqlSource解析後的SQL,大部分情況下無需修改它,只是在使用外掛是可以根據需求進行改寫。

    構建SqlSessionFactory

    有了Configuration物件,構建SqlSessionFactory是很簡單的。

 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

這裡寫圖片描述
由圖上的原始碼和之前的分析,可以得知MyBatis會根據檔案流先生成Configuration物件,進而構建SqlSessionFactory物件,真正的難點在於構建Configuration物件,所以關注的重心實際應該是Configuration物件,而不是SqlSessionFactory物件。

SqlSession執行過程

對映器(Mapper)的動態代理

在程式碼中常看到這樣一句程式碼:

UserMapper userMapper = SqlSession.getMapper(UserMapper.class); 

檢視Mybatis原始碼如何實現getMapper方法的:
實現方法
由原始碼可以得知,它運用了Configuration的物件的getMapper方法,繼續往下看:
cfg_mapper
顯然,它有用到了Mapperregistry來獲取對應的介面物件,原始碼如下:

首先它判斷是否註冊一個Mapper,沒有的話則丟擲異常,如果有,就會啟用MapperProxyFactory工廠來生成一個代理物件,繼續追蹤原始碼:
這裡寫圖片描述
注意紅圈的程式碼,Mapper對映是通過動態代理來實現的,這裡可以看到動態代理對介面的繫結,它的作用就是生成動態代理物件,而代理的方法則是放到了MapperProxy類中,因此再看一下MapperProxy的原始碼:

可以看到這裡的invoke邏輯,如果Mapper是一個動態代理物件,那麼它就會執行到invoke方法裡面,invoke方法首先判斷是否是一個類,這裡的Mapper是一個介面,所以判斷失敗。然後會生成MapperMethod物件,它是通過cachedMapperMethod方法對其初始化,最後執行execute方法,把SqlSession和當前的引數傳遞進去。execute方法的原始碼如下:
execute這裡寫圖片描述
這裡不需要全部明白所有方法的含義,只要討論executeForMany方法的實現即可。通過原始碼我們可以知道,實際上它最後就是通過SqlSession物件去執行SQL,其他的增刪改查也是類似這樣處理的。
所以,現在就清楚MyBatis為什麼只用Mapper介面便能夠執行起來了,因為Mapper的XML檔案的名稱空間對應的是這個介面的全限定名,而方法就是那條SQL的id,這樣MyBatis就可以根據全路徑和方法名,將其和代理物件繫結起來,通過動態代理技術,讓這個介面執行起來,而後採用命令模式。最後使用SQlSession介面的方法使得它能夠執行對應的SQL,只是有了這層封裝,就可以採用介面程式設計,這樣的程式設計更為簡單明瞭。