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方法,繼續往下看:
顯然,它有用到了Mapperregistry來獲取對應的介面物件,原始碼如下:
首先它判斷是否註冊一個Mapper,沒有的話則丟擲異常,如果有,就會啟用MapperProxyFactory工廠來生成一個代理物件,繼續追蹤原始碼:
注意紅圈的程式碼,Mapper對映是通過動態代理來實現的,這裡可以看到動態代理對介面的繫結,它的作用就是生成動態代理物件,而代理的方法則是放到了MapperProxy類中,因此再看一下MapperProxy的原始碼:
可以看到這裡的invoke邏輯,如果Mapper是一個動態代理物件,那麼它就會執行到invoke方法裡面,invoke方法首先判斷是否是一個類,這裡的Mapper是一個介面,所以判斷失敗。然後會生成MapperMethod物件,它是通過cachedMapperMethod方法對其初始化,最後執行execute方法,把SqlSession和當前的引數傳遞進去。execute方法的原始碼如下:
這裡不需要全部明白所有方法的含義,只要討論executeForMany方法的實現即可。通過原始碼我們可以知道,實際上它最後就是通過SqlSession物件去執行SQL,其他的增刪改查也是類似這樣處理的。
所以,現在就清楚MyBatis為什麼只用Mapper介面便能夠執行起來了,因為Mapper的XML檔案的名稱空間對應的是這個介面的全限定名,而方法就是那條SQL的id,這樣MyBatis就可以根據全路徑和方法名,將其和代理物件繫結起來,通過動態代理技術,讓這個介面執行起來,而後採用命令模式。最後使用SQlSession介面的方法使得它能夠執行對應的SQL,只是有了這層封裝,就可以採用介面程式設計,這樣的程式設計更為簡單明瞭。