您好,登錄后才能下訂單哦!
這篇文章主要介紹“Mybatis源碼分析之如何理解SQLSession初始化”,在日常操作中,相信很多人在Mybatis源碼分析之如何理解SQLSession初始化問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Mybatis源碼分析之如何理解SQLSession初始化”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
這次打算寫一個 Mybatis 源碼分析的系列,大致分為
Mybatis 啟動流程分析
Mybatis 的SQL 執行流程分析
Mybatis 的拓展點以及與 Spring Boot 的整合
這篇文章先來分析 Mybati初始化流程,如何讀取配置文件到,以及創建出 SqlSession 示例.主要內容包括
讀取、解析mybatis 全局配置文件
映射 mapper.java 文件
解析 mapper.xml 文件
解析 mapper.xml 各個節點配置,包括 namespace、緩存、增刪改查節點
Mybatis 緩存機制
構建DefaultSqlSessionFactory
SQLSession對外提供了用戶和數據庫之間交互需要的所有方法,隱藏了底層的細節。默認實現類是DefaultSqlSession
通過一個mybatis 官方提供的示例,看下如何手動創建 SQLSession
//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();
接下來就通過new SqlSessionFactoryBuilder() 開始我們的構建 SQLSession 源碼分析
//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 { //... 關閉流,拋異常 } }
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()
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; }
解析成 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); }
接下來就看看下mybatis 是如何一步步讀取配置文件的
/** * 解析 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); } }
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 中的 SQL 標簽
//解析的 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一級緩存是指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 緩存對象的時候默認是會對映射對象進行序列號操作的
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();
通過分析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 的創建已分析完畢.
總結一下上述流程:
1:通過XPathParser 讀取xml 配置文件成 XNode 屬性 2:通過 XMLConfigBuilder 解析 mybatis-config.xml 中的各個節點配置,包括
解析properties 節點
解析settings 節點
加載日志框架
解析 typeAliases
解析拓展插架 plugins
解析數據源 DataSource
解析類型處理器 typeHandle
解析 mapper文件
讀取方式有 package,resource,url,class ,最終都會放入到 Map<Class<?>, MapperProxyFactory<?>> knownMappers 中
1.同樣以輸入流的方式讀取 mapper.xml 文件 2.通過 XMLMapperBuilder 實例解析 mapper.xml 文件中各個接點屬性
解析 namespace 屬性
解析緩存引用 cache-ref
解析 cache 節點
解析 resultMap 節點
解析 sql 節點
解析 select | insert |update |delete節點 3.通過 XMLStatementBuilder 解析SQL 標簽
到此,關于“Mybatis源碼分析之如何理解SQLSession初始化”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。