精盡MyBatis原始碼分析 - MyBatis初始化(二)之載入 Mapper 介面與 XML 對映檔案
阿新 • • 發佈:2020-11-23
> 該系列文件是本人在學習 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**配置(` `標籤)
- `org.apache.ibatis.builder.MapperBuilderAssistant`:Mapper構造器小助手,用於建立ResultMapping、ResultMap和MappedStatement物件
- `org.apache.ibatis.mapping.ResultMapping`:儲存` `標籤的子標籤相關資訊,也就是 Java Type 與 Jdbc Type 的對映資訊
- `org.apache.ibatis.mapping.ResultMap`:儲存了` `標籤的配置資訊以及子標籤的所有資訊
- `org.apache.ibatis.mapping.MappedStatement`:儲存瞭解析` `標籤內的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`中進行解析,感興趣的小夥伴可以看看