此次打算寫一個 Mybatis 源碼分析的系列,大體分爲html
- Mybatis 啓動流程分析
- Mybatis 的SQL 執行流程分析
- Mybatis 的拓展點以及與 Spring Boot 的整合
這篇文章先來分析 Mybati初始化流程,如何讀取配置文件到,以及建立出 SqlSession 示例.主要內容包括java
- 讀取、解析mybatis 全局配置文件
- 映射 mapper.java 文件
- 解析 mapper.xml 文件
- 解析 mapper.xml 各個節點配置,包括 namespace、緩存、增刪改查節點
- Mybatis 緩存機制
- 構建DefaultSqlSessionFactory
什麼是 SQLSession
SQLSession對外提供了用戶和數據庫之間交互須要的全部方法,隱藏了底層的細節。默認實現類是DefaultSqlSessionnode
SQLSession 建立示例
經過一個mybatis 官方提供的示例,看下如何手動建立 SQLSessionmysql
//Mybatis 配置文件,一般包含:數據庫鏈接信息,Mapper.class 全限定名包路徑,事務配置,插件配置等等 String resource = "org/mybatis/builder/mybatis-config.xml"; //以輸入流的方式讀取配置 InputStream inputStream = Resources.getResourceAsStream(resource); //實例化出 SQLSession 的必要步驟 SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(inputStream); SqlSession session = factory.openSession();
經過輸入流讀取mybatis-config.xml 配置文件
接下來就經過new SqlSessionFactoryBuilder() 開始咱們的構建 SQLSession 源碼分析spring
//SqlSessionFactory 有4 個構造方法,最終都會執行到全參的構造方法 public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { //首先會實例化一個 XMLConfigBuilder ,這裏先有個基本的認知:XMLConfigBuilder 就是用來解析 XML 文件配置的 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); //通過parser.parse()以後,XML配置文件已經被解析成了Configuration ,Configuration 對象是包含着mybatis的全部屬性. return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { //... 關閉流,拋異常 } }
插句嘴,先看下XMLConfigBuilder 類圖
- MLConfigBuilder : 解析全局配置文件即 mybatis-config.xml
- XMLMapperBuilder : 解析 Mapper 文件,配置在mybatis-config.xml 文件中 mapper.java 的包路徑
- XMLStatementBuilder :解析 mapper 文件的節點中 ,SQL 語句標籤:select,update,insert,delete
- SQLSourceBuilder:動態解析 SQL 語句,根據 SqlNode 解析 Sql 語句中的標籤,好比<trim>,<if>等標籤
固然 BaseBuilder 的實現類不只這 4 個,這裏只介紹這 4 類,在後續一步步分析中都能看到這幾個的身影 點進去看一下 parser.parse()sql
public Configuration parse() { // 若已經解析過了 就拋出異常 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 設置解析標誌位 parsed = true; // 解析mybatis-config.xml的節點,讀取配置文件,加載到 Configuration 中 parseConfiguration(parser.evalNode("/configuration")); return configuration; }
經過XPathParser 對象來解析 xml 文件成XNode 對象
解析成 Configuration 成以前會先將 xml 配置文件解析成 XNode 對象數據庫
public XNode evalNode(Object root, String expression) { //mybatis 自已定義了一個XPathParser 對象來解析 xml ,其實對Document作了封裝 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } return new XNode(this, node, variables); }
解析config.xml 中的各個節點
接下來就看看下mybatis 是如何一步步讀取配置文件的express
/** * 解析 mybatis-config.xml的 configuration節點 * 解析 XML 中的各個節點 */ private void parseConfiguration(XNode root) { try { /** * 解析 properties節點 * <properties resource="mybatis/db.properties" /> * 解析到org.apache.ibatis.parsing.XPathParser#variables * org.apache.ibatis.session.Configuration#variables */ propertiesElement(root.evalNode("properties")); /** * 解析咱們的mybatis-config.xml中的settings節點 * 具體能夠配置哪些屬性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings * <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="mapUnderscoreToCamelCase" value="false"/> <setting name="localCacheScope" value="SESSION"/> <setting name="jdbcTypeForNull" value="OTHER"/> .............. </settings> * */ Properties settings = settingsAsProperties(root.evalNode("settings")); /** * 基本沒有用過該屬性 * VFS含義是虛擬文件系統;主要是經過程序可以方便讀取本地文件系統、FTP文件系統等系統中的文件資源。 Mybatis中提供了VFS這個配置,主要是經過該配置能夠加載自定義的虛擬文件系統應用程序 解析到:org.apache.ibatis.session.Configuration#vfsImpl */ loadCustomVfs(settings); /** * 指定 MyBatis 所用日誌的具體實現,未指定時將自動查找。 * SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING * 解析到org.apache.ibatis.session.Configuration#logImpl */ loadCustomLogImpl(settings); /** * 解析咱們的別名 * <typeAliases> <typeAlias alias="User" type="com.xxx.entity.User"/> </typeAliases> <typeAliases> <package name="com.xxx.use"/> </typeAliases> 解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases */ typeAliasesElement(root.evalNode("typeAliases")); /** * 解析咱們的插件(好比分頁插件) * mybatis自帶的 * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query) 解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors */ pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); // 設置settings 和默認值 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 /** * 解析咱們的mybatis環境,解析 DataSource <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="root"/> <property name="password" value="Zw726515"/> </dataSource> </environment> <environment id="test"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> * 解析到:org.apache.ibatis.session.Configuration#environment * 在集成spring狀況下由 spring-mybatis提供數據源 和事務工廠 */ environmentsElement(root.evalNode("environments")); /** * 解析數據庫廠商 * <databaseIdProvider type="DB_VENDOR"> <property name="SQL Server" value="sqlserver"/> <property name="DB2" value="db2"/> <property name="Oracle" value="oracle" /> <property name="MySql" value="mysql" /> </databaseIdProvider> * 解析到:org.apache.ibatis.session.Configuration#databaseId */ databaseIdProviderElement(root.evalNode("databaseIdProvider")); /** * 解析咱們的類型處理器節點 * <typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers> 解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap */ typeHandlerElement(root.evalNode("typeHandlers")); /** * 最最重要的就是解析咱們的mapper * resource:來註冊咱們的class類路徑下的 url:來指定咱們磁盤下的或者網絡資源的 class: 若註冊Mapper不帶xml文件的,這裏能夠直接註冊 若註冊的Mapper帶xml文件的,須要把xml文件和mapper文件同名 同路徑 --> <mappers> <mapper resource="mybatis/mapper/EmployeeMapper.xml"/> <mapper class="com.tuling.mapper.DeptMapper"></mapper> <package name="com.tuling.mapper"></package> --> </mappers> * 解析 mapper: * 1.解析mapper.java接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers * 2.解析 mapper.xml 配置 */ mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
解析 mapper 文件
解析 mapper.java 接口到knowMappers 中
private void mapperElement(XNode parent) throws Exception { if (parent != null) { //獲取咱們mappers節點下的一個一個的mapper節點 for (XNode child : parent.getChildren()) { /** * 指定 mapper 的 4 中方式: * 1.指定的 mapper 所在的包路徑,批量註冊 * 2.經過 resource 目錄指定 * 3.經過 url 指定,從網絡資源或者本地磁盤 * 4.經過 class 路徑註冊 */ //判斷咱們mapper是否是經過批量註冊的 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { //判斷從classpath下讀取咱們的mapper String resource = child.getStringAttribute("resource"); //判斷是否是從咱們的網絡資源讀取(或者本地磁盤得) String url = child.getStringAttribute("url"); //解析這種類型(要求接口和xml在同一個包下) String mapperClass = child.getStringAttribute("class"); //解析 mapper 文件 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 把mapper文件讀取出一個流,是否是似曾相識,最開始的時候讀取mybatis-config.xml 配置文件也是經過輸入流的方式讀取的 InputStream inputStream = Resources.getResourceAsStream(resource); //建立讀取XmlMapper構建器對象,用於來解析咱們的mapper.xml文件,上面提到過的 XMLMapperBuilder對象 /** * 讀取的 mapper 文件會被放入到 MapperRegistry 中的 knownMappers中 * Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); * 爲後續建立 Mapper 代理對象作準備 */ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //真正的解析咱們的mapper.xml配置文件,這裏就會來解析咱們的sql mapperParser.parse(); } 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(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
解析 mapper.xml 文件
解析Mapper.xml 中的 SQL 標籤apache
//解析的 SQL 語句節點會放在Configuration.MappedStatement.SqlSource 中,SqlSource 中包含了一個個的 SQLNode,一個標籤對應一個 SQLNode public void parse() { //判斷當前的Mapper是否被加載過 if (!configuration.isResourceLoaded(resource)) { //真正的解析咱們的mapper configurationElement(parser.evalNode("/mapper")); //把資源保存到咱們Configuration中 configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
解析 mapper.xml 中的各個節點緩存
//解析咱們的<mapper></mapper>節點 private void configurationElement(XNode context) { try { /** * 解析咱們的namespace屬性 * <mapper namespace="com.xx.mapper.xxxMapper"> */ String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } //保存咱們當前的namespace 而且判斷接口徹底類名==namespace builderAssistant.setCurrentNamespace(namespace); /** * 解析咱們的緩存引用 * 說明我當前的緩存引用和DeptMapper的緩存引用一致 * <cache-ref namespace="com.xx.mapper.xxxMapper"></cache-ref> 解析到org.apache.ibatis.session.Configuration#cacheRefMap<當前namespace,ref-namespace> 異常下(引用緩存未使用緩存):org.apache.ibatis.session.Configuration#incompleteCacheRefs */ cacheRefElement(context.evalNode("cache-ref")); /** * 解析咱們的cache節點 * <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache> 解析到:org.apache.ibatis.session.Configuration#caches org.apache.ibatis.builder.MapperBuilderAssistant#currentCache */ cacheElement(context.evalNode("cache")); /** * 解析paramterMap節點 */ parameterMapElement(context.evalNodes("/mapper/parameterMap")); /** * 解析咱們的resultMap節點 * 解析到:org.apache.ibatis.session.Configuration#resultMaps * 異常 org.apache.ibatis.session.Configuration#incompleteResultMaps * */ resultMapElements(context.evalNodes("/mapper/resultMap")); /** * 解析咱們經過sql節點 * 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments * 其實等於 org.apache.ibatis.session.Configuration#sqlFragments * 由於他們是同一引用,在構建XMLMapperBuilder 時把Configuration.getSqlFragments傳進去了 */ sqlElement(context.evalNodes("/mapper/sql")); /** * 解析咱們的select | insert |update |delete節點 * 解析到org.apache.ibatis.session.Configuration#mappedStatements * 最終SQL節點會被解析成 MappedStatement,一個節點就是對應一個MappedStatement * 準確的說 sql 節點被解析成 SQLNode 封裝在 MappedStatement.SqlSource 中 * SQLNode 對應的就是 sql 節點中的子標籤,好比<trim>,<if>,<where> 等 */ 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); } }
着重分析幾個解析過程
解析緩存
private void cacheElement(XNode context) { if (context != null) { /** * cache元素可指定以下屬性,每種屬性的指定都是針對都是針對底層Cache的一種裝飾,採用的是裝飾器的模式 * 緩存屬性: * 1.eviction: 緩存過時策略:默認是LRU * LRU – 最近最少使用的:移除最長時間不被使用的對象。--> LruCache * FIFO – 先進先出:按對象進入緩存的順序來移除它們。--> FifoCache * SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。--> SoftCache * WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象。--> WeakCache * 2.flushInterval: 清空緩存的時間間隔,單位毫秒,默認不清空,指定了以後將會用 ScheduleCache 封裝 * 3.size :緩存對象的大小,默認是 1024,其是針對LruCache而言的,LruCache默認只存儲最多1024個Key * 4.readOnly :默認是false,底層SerializedCache包裝,會在寫緩存的時候將緩存對象進行序列化,而後在讀緩存的時候進行反序列化,這樣每次讀到的都將是一個新的對象,即便你更改了讀取到的結果,也不會影響原來緩存的對象;true-給全部調用者返回緩存對象的相同實例 * 5.blocking : 默認爲false,當指定爲true時將採用BlockingCache進行封裝,在進行增刪改以後的併發查詢,只會有一條去數據庫查詢,而不會併發訪問 * 6.type: type屬性用來指定當前底層緩存實現類,默認是PerpetualCache,若是咱們想使用自定義的Cache,則能夠經過該屬性來指定,對應的值是咱們自定義的Cache的全路徑名稱 */ //解析cache節點的type屬性 String type = context.getStringAttribute("type", "PERPETUAL"); //根據type的String獲取class類型 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); //獲取緩存過時策略:默認是LRU String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); //flushInterval(刷新間隔)屬性能夠被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認狀況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。 Long flushInterval = context.getLongAttribute("flushInterval"); //size(引用數目)屬性能夠被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024。 Integer size = context.getIntAttribute("size"); //只讀)屬性能夠被設置爲 true 或 false。只讀的緩存會給全部調用者返回緩存對象的相同實例。 所以這些對象不能被修改。這就提供了可觀的性能提高。而可讀寫的緩存會(經過序列化)返回緩存對象的拷貝。 速度上會慢一些,可是更安全,所以默認值是 false boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); //把緩存節點加入到Configuration中 //這裏的 builder()方法利用責任鏈方式循環實例化Cache 對象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
Mybatis 緩存機制
MyBatis自帶的緩存有一級緩存和二級緩存
一級緩存
Mybatis一級緩存是指Session緩存。做用域默認是一個SqlSession。默認開啓一級緩存,範圍有SESSION和STATEMENT兩種,默認是SESSION,若是須要更改一級緩存的範圍,能夠在Mybatis的配置文件中,經過localCacheScope指定
<setting name="localCacheScope" value="STATEMENT"/>
二級緩存
Mybatis的二級緩存是指mapper映射文件。二級緩存的做用域是同一個namespace下的mapper映射文件內容,多個SqlSession共享。二級緩存是默認啓用的,可是須要手動在 mapper 文件中設置啓動二級緩存
//在 mapper.xml 文件加上此配置,該 mapper 文件對應的 SQL就開啓了緩存 <cache />
或者直接關閉緩存
//在全局配置文件中關閉緩存 <settings> <setting name="cacheEnabled" value="false" /> </settings>
注意:若是開啓了二級緩存,查詢結果的映射對象必定要實現Serializable ,由於mybatis 緩存對象的時候默認是會對映射對象進行序列號操做的
解析select | insert |update |delete節點
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { //循環咱們的select|delte|insert|update節點 for (XNode context : list) { //建立一個xmlStatement的構建器對象 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //經過該步驟解析以後 mapper.xml 的 sql 節點就也被解析了 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
至此配置mybatis 的配置文件已經解析完成,配置文件已經解析成了Configuration,會到最初,咱們的目標是獲取 SqlSession 對象,經過new SqlSessionFactoryBuilder().build(reader) 已經構建出了一個SqlSessionFactory 工廠對象,還差一步 SqlSession session = sqlMapper.openSession();
根據 Configuration build 出 DefaultSqlSessionFactory
經過分析DefaultSqlSession 的 openSession() 來實例化 SQLSession 對象
//從session中開啓一個數據源 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //獲取環境變量 final Environment environment = configuration.getEnvironment(); // 獲取事務工廠 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); /** * 建立一個sql執行器對象 * 通常狀況下 若咱們的mybaits的全局配置文件的cacheEnabled默認爲ture就返回 * 一個cacheExecutor,若關閉的話返回的就是一個SimpleExecutor */ final Executor executor = configuration.newExecutor(tx, execType); //建立返回一個DeaultSqlSessoin對象返回 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
仔細一點看你會發現configuration 就是剛纔千辛萬苦建立出來的 Configuration 對象,包含全部 mybatis 配置信息.至此SQLSession 的建立已分析完畢.
總結
總結一下上述流程:
第一步:經過輸入流讀取mybatis-config.xml 配置文件
1:經過XPathParser 讀取xml 配置文件成 XNode 屬性 2:經過 XMLConfigBuilder 解析 mybatis-config.xml 中的各個節點配置,包括
- 解析properties 節點
- 解析settings 節點
- 加載日誌框架
- 解析 typeAliases
- 解析拓展插架 plugins
- 解析數據源 DataSource
- 解析類型處理器 typeHandle
- 解析 mapper文件
第二步:讀取 mapper.java 類
讀取方式有 package,resource,url,class ,最終都會放入到 Map<Class<?>, MapperProxyFactory<?>> knownMappers 中
第三步: 讀取 mapper.xml 節點
1.一樣以輸入流的方式讀取 mapper.xml 文件 2.經過 XMLMapperBuilder 實例解析 mapper.xml 文件中各個接點屬性
- 解析 namespace 屬性
- 解析緩存引用 cache-ref
- 解析 cache 節點
- 解析 resultMap 節點
- 解析 sql 節點
- 解析 select | insert |update |delete節點 3.經過 XMLStatementBuilder 解析SQL 標籤