1. 程式人生 > >精盡MyBatis原始碼分析 - MyBatis初始化(二)之載入 Mapper 介面與 XML 對映檔案

精盡MyBatis原始碼分析 - MyBatis初始化(二)之載入 Mapper 介面與 XML 對映檔案

> 該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋([Mybatis原始碼分析 GitHub 地址](https://github.com/liu844869663/mybatis-3)、[Mybatis-Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring)、[Spring-Boot-Starter 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-boot-starter))進行閱讀 > > MyBatis 版本:3.5.2 > > MyBatis-Spring 版本:2.0.3 > > MyBatis-Spring-Boot-Starter 版本:2.1.4 ## MyBatis的初始化 在MyBatis初始化過程中,大致會有以下幾個步驟: 1. 建立`Configuration`全域性配置物件,會往`TypeAliasRegistry`別名註冊中心新增Mybatis需要用到的相關類,並設定預設的語言驅動類為`XMLLanguageDriver` 2. 載入`mybatis-config.xml`配置檔案、Mapper介面中的註解資訊和XML對映檔案,解析後的配置資訊會形成相應的物件並儲存到Configuration全域性配置物件中 3. 構建`DefaultSqlSessionFactory`物件,通過它可以建立`DefaultSqlSession`物件,MyBatis中`SqlSession`的預設實現類 因為整個初始化過程涉及到的程式碼比較多,所以拆分成了四個模組依次對MyBatis的初始化進行分析: - [**《MyBatis初始化(一)之載入mybatis-config.xml》**](https://www.cnblogs.com/lifullmoon/p/14015009.html) - [**《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》**](https://www.cnblogs.com/lifullmoon/p/14015046.html) - **《MyBatis初始化(三)之SQL初始化(上)》** - **《MyBatis初始化(四)之SQL初始化(下)》** 由於在MyBatis的初始化過程中去解析Mapper介面與XML對映檔案涉及到的篇幅比較多,XML對映檔案的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解 ## 初始化(二)之載入Mapper介面與對映檔案 在上一個模組已經分析了是如何解析`mybatis-config.xml`配置檔案的,在最後如何解析`
`標籤的還沒有進行分析,這個過程稍微複雜一點,因為需要解析Mapper介面以及它的XML對映檔案,讓我們一起來看看這個解析過程 解析XML對映檔案生成的物件主要如下圖所示: 主要包路徑:org.apache.ibatis.builder、org.apache.ibatis.mapping 主要涉及到的類: - `org.apache.ibatis.builder.xml.XMLConfigBuilder`:根據配置檔案進行解析,開始Mapper介面與XML對映檔案的初始化,生成Configuration全域性配置物件 - `org.apache.ibatis.binding.MapperRegistry`:Mapper介面註冊中心,將Mapper介面與其動態代理物件工廠進行儲存,這裡我們解析到的Mapper介面需要往其進行註冊 - `org.apache.ibatis.builder.annotation.MapperAnnotationBuilder`:解析Mapper介面,主要是解析介面上面註解,其中載入XML對映檔案內部會呼叫`XMLMapperBuilder`類進行解析 - `org.apache.ibatis.builder.xml.XMLMapperBuilder`:解析XML對映檔案 - `org.apache.ibatis.builder.xml.XMLStatementBuilder`:解析XML對映檔案中的**Statement**配置(`
`標籤內的SQL語句所生成的所有資訊 ### 解析入口 我們回顧上一個模組,在`org.apache.ibatis.builder.xml.XMLConfigBuilder`中會解析mybatis-config.xml配置檔案中的``標籤,呼叫其`parse()`->`parseConfiguration(XNode root)`->`mapperElement(XNode parent)`方法,那麼我們來看看這個方法,程式碼如下: ```java private void mapperElement(XNode parent) throws Exception { if (parent != null) { // <0> 遍歷子節點 for (XNode child : parent.getChildren()) { // <1> 如果是 package 標籤,則掃描該包 if ("package".equals(child.getName())) { // 獲得包名 String mapperPackage = child.getStringAttribute("name"); // 新增到 configuration 中 configuration.addMappers(mapperPackage); } else { // 如果是 mapper 標籤 // 獲得 resource、url、class 屬性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // <2> 使用相對於類路徑的資源引用 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 獲得 resource 的 InputStream 物件 InputStream inputStream = Resources.getResourceAsStream(resource); // 建立 XMLMapperBuilder 物件 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 執行解析 mapperParser.parse(); // <3> 使用完全限定資源定位符(URL) } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); // 獲得 url 的 InputStream 物件 InputStream inputStream = Resources.getUrlAsStream(url); // 建立 XMLMapperBuilder 物件 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,configuration.getSqlFragments()); // 執行解析 mapperParser.parse(); // <4> 使用對映器介面實現類的完全限定類名 } else if (resource == null && url == null && mapperClass != null) { // 獲得 Mapper 介面 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."); } } } } } ``` 遍歷``標籤的子節點 1. 如果是``子節點,則獲取`package`屬性,對該包路徑下的Mapper介面進行解析 2. 否的的話,通過子節點的`resource`屬性或者`url`屬性解析該對映檔案,或者通過`class`屬性解析該Mapper介面 通常我們是直接配置一個包路徑,這裡就檢視上面第`1`種對Mapper介面進行解析的方式,第`2`種的解析方式其實在第`1` 種方式都會涉及到,它只是抽取出來了,那麼我們就直接看第`1`種方式 首先將`package`包路徑新增到`Configuration`全域性配置物件中,也就是往其內部的`MapperRegistry`登錄檔進行註冊,呼叫它的`MapperRegistry`的`addMappers(String packageName)`方法進行註冊 我們來看看在MapperRegistry登錄檔中是如何解析的,在之前文件的**Binding模組**中有講到過這個類,該方法如下: ```java public class MapperRegistry { public void addMappers(String packageName) { addMappers(packageName, Object.class); } /** * 用於掃描指定包中的Mapper介面,並與XML檔案進行繫結 * @since 3.2.2 */ public void addMappers(String packageName, Class superType) { // <1> 掃描指定包下的指定類 ResolverUtil> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set>> mapperSet = resolverUtil.getClasses(); // <2> 遍歷,新增到 knownMappers 中 for (Class mapperClass : mapperSet) { addMapper(mapperClass); } } public void addMapper(Class type) { // <1> 判斷,必須是介面。 if (type.isInterface()) { // <2> 已經新增過,則丟擲 BindingException 異常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // <3> 將Mapper介面對應的代理工廠新增到 knownMappers 中 knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the mapper parser. // If the type is already known, it won't try. // <4> 解析 Mapper 的註解配置 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 解析 Mapper 介面上面的註解和 Mapper 介面對應的 XML 檔案 parser.parse(); // <5> 標記載入完成 loadCompleted = true; } finally { // <6> 若載入未完成,從 knownMappers 中移除 if (!loadCompleted) { knownMappers.remove(type); } } } } } ``` `<1>`首先必須是個介面 `<2>`已經在`MapperRegistry`註冊中心存在,則會丟擲異常 `<3>`建立一個Mapper介面對應的`MapperProxyFactory`動態代理工廠 `<4>`【**重要!!!**】通過`MapperAnnotationBuilder`解析該Mapper介面與對應XML對映檔案 ### MapperAnnotationBuilder `org.apache.ibatis.builder.annotation.MapperAnnotationBuilder`:解析Mapper介面,主要是解析介面上面註解,載入XML檔案會呼叫XMLMapperBuilder類進行解析 我們先來看看他的建構函式和`parse()`解析方法: ```java public class MapperAnnotationBuilder { /** * 全域性配置物件 */ private final Configuration configuration; /** * Mapper 構造器小助手 */ private final MapperBuilderAssistant assistant; /** * Mapper 介面的 Class 物件 */ private final Class type; public MapperAnnotationBuilder(Configuration configuration, Class type) { String resource = type.getName().replace('.', '/') + ".java (best guess)"; this.assistant = new MapperBuilderAssistant(configuration, resource); this.configuration = configuration; this.type = type; } public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { // 載入該介面對應的 XML 檔案 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); // 解析 Mapper 介面的 @CacheNamespace 註解,建立快取 parseCache(); // 解析 Mapper 介面的 @CacheNamespaceRef 註解,引用其他名稱空間 parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { // 如果不是橋接方法 // 解析方法上面的註解 parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); } private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; // #1347 InputStream inputStream = type.getResourceAsStream("/" + xmlResource); if (inputStream == null) { // Search XML mapper that is not in the module but in the classpath. try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e2) { // ignore, resource is not required } } if (inputStream != null) { // 建立 XMLMapperBuilder 物件 XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); // 解析該 XML 檔案 xmlParser.parse(); } } } } ``` 在建構函式中,會建立一個`MapperBuilderAssistant`物件,Mapper 構造器小助手,用於建立XML對映檔案中對應相關物件 `parse()`方法,用於解析Mapper介面: 1. 獲取Mapper介面的名稱,例如`interface xxx.xxx.xxx`,根據Configuration全域性配置物件判斷該Mapper介面是否被解析過 2. 沒有解析過則呼叫`loadXmlResource()`方法解析對應的XML對映檔案 3. 然後解析介面的@CacheNamespace和@CacheNamespaceRef註解,再依次解析方法上面的MyBatis相關注解 註解的相關解析這裡就不講述了,因為我們通常都是使用XML對映檔案,邏輯沒有特別複雜,都在`MapperAnnotationBuilder`中進行解析,感興趣的小夥伴可以看看