Mybatis3詳解(十六)——Mybatis執行原理之SqlSessionFactory的構建過程
1、寫在前面
前面的一系列文章已經詳細的介紹了Mybatis的各種使用方法,所以這章我們來更加深入的瞭解Mybatis,講述一下Mybatis的內部解析與執行原理,但是這章所講的只涉及基本的框架和核心程式碼,並不會面面俱到,所以本章中的一些細節將會被忽略掉,需要仔細研究的可以自行查閱相關書籍或者問度娘。雖然這章不可能讓你對Mybatis的所有知識點都瞭解,但是當我們掌握了Mybatis的執行原理,就可以知道Mybatis是怎麼執行的,也為後面大家閱讀Mybatis原始碼奠定一點基礎吧。
Mybatis的執行分為兩大部分:
- 一是SqlSessionFactory的建立過程,它主要是通過XMLConfigBuilder將我們的配置檔案讀取並且快取到Configuration物件中,然後通過Configuration來建立SqlSessionFactory物件。
- 二是SqlSession的執行過程,這個過程是Mybatis中最複雜的,它包含了許多複雜的技術,包括反射技術和動態代理技術等,這是Mybatis底層架構的基礎。
MyBatis的主要成員元件(成員):
在第一章的時候,簡單的介紹了Mybatis的有哪些元件(成員),這裡再次詳細的介紹一下:
- SqlSessionFactoryBuilder:會根據XML配置或是Java配置來生成SqlSessionFactory物件。採用建造者模式(簡單來說就是分步構建一個大的物件,例如建造一個大房子,採用購買磚頭、砌磚、粉刷牆面的步驟建造,其中的大房子就是大物件,一系列的建造步驟就是分步構建)。
- SqlSessionFactory:用於生成SqlSession,可以通過 SqlSessionFactory.openSession() 方法建立 SqlSession 物件。使用工廠模式(簡單來說就是我們獲取物件是通過一個類,由這個類去建立我們所需的例項並返回,而不是我們自己通過new去建立)。
- Configuration:MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中。
- SqlSession:相當於JDBC中的 Connection物件,可以用 SqlSession 例項來直接執行被對映的 SQL 語句,也可以獲取對應的Mapper。
- Executor:MyBatis 中所有的 Mapper 語句的執行都是通過 Executor 執行的,負責SQL語句的生成和查詢快取的維護 。
- StatementHandler:封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等。
- ParameterHandler:負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別。
- ResultSetHandler:負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合。
- TypeHandler:負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換。
- MappedStatement:作用是儲存一個對映器節點<select|update|delete|insert>中的內容。MappedStatement封裝了Statement的相關資訊,包括我們配置的SQL、SQL的id、快取資訊、resultMap、ParameterType、resultType、resultMap等重要配置內容等。Mybatis可以通過它來獲取某條SQL配置的所有資訊。它還有一個非常重要的屬性是SqlSource。
- 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語句的一箇中轉站吧。
- BoundSql:它是一個結果物件,它是通過SqlSource來獲取的。作用是通過SqlSource對對映檔案的SQL和引數聯合解析得到的真正SQL和引數。什麼意思呢?就是BoundSql包含了真正的SQL語句(由SqlSource生成的,如select * from t_user where id = ?),而且還包含了SQL語句增刪改查的引數,而SqlSource是負責將對映檔案中定義的SQL生成真正的SQL語句(算是對映檔案中的SQL生成真正的SQL語句的中轉站),這裡搞得我有點昏 。BoundSql有3個常用的屬性:sql、parameterObject、parameterMappings,這裡就不做討論了,通過名字應該很容易理解它的用處。
以上主要元件(成員)在一次資料庫操作中基本都會涉及。
注:圖片來自《》
2、SqlSessionFactory的構建過程
SqlSessionFactory 是MyBatis的核心類之一, 其最重要的功能就是提供建立MyBatis的核心介面SqlSession,所以我們要先建立SqlSessionFactory,它是通過Builder(建造者)模式來建立的,所以在Mybatis中提供了SqlSessionFactoryBuilder類。其構建分為兩步。
- 第 1 步: 通過 org.apache.ibatis.builder.xml.XMLConfigBuilder 解析配置的XML檔案,讀出所配置的引數,並將讀取的內容存入org.apache.ibatis.session.Configuration類物件中。而Configuration採用的是單例模式,幾乎所有的 MyBatis 配置內容都會存放在這個單例物件中,以便後續將這些內容讀出。
- 第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。我們來看一下這個類構造方法:
通過檢視XMLConfigBuilder中構造方法的原始碼,可以得知XML配置檔案最終是由org.apache.ibatis.parsing.XPathParser封裝的XPath解析的。第一個構造方法通過XPathParser構造方法傳入我們讀取的XML流檔案、Properites流檔案和environment等引數得到了一個XpathParser例項物件parser,這裡parser已包含全域性XML配置檔案解析後的所有資訊,然後再將parser作為引數傳給XMLConfigBuilder構造方法。其中XMLConfigBuilder 構造方法還呼叫了父類BaseBuilder的構造方法BaseBuilder(Configuration),這裡傳入了一個Configuration物件,用來初始化Configuration物件,我們來繼續進入看一下:
注意:這裡的重點是建立了一個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屬性方式引入對映器的構造方法(它兩共用一個):
XPathParser將mapper配置檔案解析成Document物件後封裝到一個XPathParser物件,再將XPathParser物件作為引數傳給XMLMapperBuilder構造方法並構造出一個XMLMapperBuilder物件,XMLMapperBuilder物件的builderAssistant欄位是一個MapperBuilderAssistant物件,同樣也是BaseBuilder的一個子類,其作用是對MappedStatement物件進行封裝。
有了XMLMapperBuilder物件後,就可以進入解析mapper對映檔案的過程,進入parse()方法:
呼叫XMLMapperBuilder的configurationElement方法,對mapper對映檔案進行解析
mapper對映檔案必須有namespace屬性值,否則丟擲異常,將namespace屬性儲存到XMLMapperBuilder的MapperBuilderAssistant物件中,以便其他方法呼叫。
該方法對mapper對映檔案每個標籤逐一解析並儲存到Configuration和MapperBuilderAssistant物件中,最後呼叫buildStatementFromContext方法解析select、insert、update和delete節點。
buildStatementFromContext方法中呼叫XMLStatementBuilder的parseStatementNode()方法來完成解析。
注意:解析所有的Sql語句會封裝一個MappedStatement中,MappedStatement中包含了許多我們配置的SQL、SQL的id、快取資訊、resultMap、ParameterType、resultType、resultMap等重要配置內容。最重要的是它還有一個屬性sqlSource。
通過上面的方法可以看到SQL語句封裝到一個SqlSource物件,SqlSource是個介面,如果是動態SQL就建立DynamicSqlSource實現類,否則建立StaticSqlSource實現類。
SqlSource是MappedStatement的一個屬性,它只是一個介面。它的主要作用是根據上下文和引數解析生成需要的Sql。
SqlSource介面中有一個getBoundSql方法,這個方法就是用來獲取BoundSql的:
SqlSource介面中還有如下這幾個重要的實現類:
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構建過程中的時序圖:
參考連結:
- 《Java EE 網際網路輕量級框架整合開發》