精盡MyBatis源碼分析 – MyBatis初始化(二)之載入 Mapper 介面與 XML 映射文件
- 2020 年 11 月 23 日
- 筆記
- mybatis, 源碼解析, 精盡MyBatis源碼分析
該系列文檔是本人在學習 Mybatis 的源碼過程中總結下來的,可能對讀者不太友好,請結合我的源碼注釋(Mybatis源碼分析 GitHub 地址、Mybatis-Spring 源碼分析 GitHub 地址、Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的初始化
在MyBatis初始化過程中,大致會有以下幾個步驟:
-
創建
Configuration
全局配置對象,會往TypeAliasRegistry
別名註冊中心添加Mybatis需要用到的相關類,並設置默認的語言驅動類為XMLLanguageDriver
-
載入
mybatis-config.xml
配置文件、Mapper介面中的註解資訊和XML映射文件,解析後的配置資訊會形成相應的對象並保存到Configuration全局配置對象中 -
構建
DefaultSqlSessionFactory
對象,通過它可以創建DefaultSqlSession
對象,MyBatis中SqlSession
的默認實現類
因為整個初始化過程涉及到的程式碼比較多,所以拆分成了四個模組依次對MyBatis的初始化進行分析:
- 《MyBatis初始化(一)之載入mybatis-config.xml》
- 《MyBatis初始化(二)之載入Mapper介面與XML映射文件》
- 《MyBatis初始化(三)之SQL初始化(上)》
- 《MyBatis初始化(四)之SQL初始化(下)》
由於在MyBatis的初始化過程中去解析Mapper介面與XML映射文件涉及到的篇幅比較多,XML映射文件的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解
初始化(二)之載入Mapper介面與映射文件
在上一個模組已經分析了是如何解析mybatis-config.xml
配置文件的,在最後如何解析<mapper />
標籤的還沒有進行分析,這個過程稍微複雜一點,因為需要解析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配置(<select /> <update /> <delete /> <insert />
標籤)org.apache.ibatis.builder.MapperBuilderAssistant
:Mapper構造器小助手,用於創建ResultMapping、ResultMap和MappedStatement對象org.apache.ibatis.mapping.ResultMapping
:保存<resultMap />
標籤的子標籤相關資訊,也就是 Java Type 與 Jdbc Type 的映射資訊org.apache.ibatis.mapping.ResultMap
:保存了<resultMap />
標籤的配置資訊以及子標籤的所有資訊org.apache.ibatis.mapping.MappedStatement
:保存了解析<select /> <update /> <delete /> <insert />
標籤內的SQL語句所生成的所有資訊
解析入口
我們回顧上一個模組,在org.apache.ibatis.builder.xml.XMLConfigBuilder
中會解析mybatis-config.xml配置文件中的<mapper />
標籤,調用其parse()
->parseConfiguration(XNode root)
->mapperElement(XNode parent)
方法,那麼我們來看看這個方法,程式碼如下:
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.");
}
}
}
}
}
遍歷<mapper />
標籤的子節點
-
如果是
<package />
子節點,則獲取package
屬性,對該包路徑下的Mapper介面進行解析 -
否的的話,通過子節點的
resource
屬性或者url
屬性解析該映射文件,或者通過class
屬性解析該Mapper介面
通常我們是直接配置一個包路徑,這裡就查看上面第1
種對Mapper介面進行解析的方式,第2
種的解析方式其實在第1
種方式都會涉及到,它只是抽取出來了,那麼我們就直接看第1
種方式
首先將package
包路徑添加到Configuration
全局配置對象中,也就是往其內部的MapperRegistry
註冊表進行註冊,調用它的MapperRegistry
的addMappers(String packageName)
方法進行註冊
我們來看看在MapperRegistry註冊表中是如何解析的,在之前文檔的Binding模組中有講到過這個類,該方法如下:
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<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
// <2> 遍歷,添加到 knownMappers 中
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public <T> void addMapper(Class<T> 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()
解析方法:
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介面:
-
獲取Mapper介面的名稱,例如
interface xxx.xxx.xxx
,根據Configuration全局配置對象判斷該Mapper介面是否被解析過 -
沒有解析過則調用
loadXmlResource()
方法解析對應的XML映射文件 -
然後解析介面的@CacheNamespace和@CacheNamespaceRef註解,再依次解析方法上面的MyBatis相關註解
註解的相關解析這裡就不講述了,因為我們通常都是使用XML映射文件,邏輯沒有特別複雜,都在MapperAnnotationBuilder
中進行解析,感興趣的小夥伴可以看看😈😈😈
loadXmlResource()
方法,解析Mapper介面對應的XML映射文件:
- 根據Configuration全局配置對象判斷該Mapper介面對應的XML映射文件是否被解析過,例如判斷
namespace:xxx.xxx.xxx
是否在已載入的資源中 - 獲取XML映射文件資源,例如:獲取
xxx/xxx/xxx.xml
文件流,與介面名稱對應 - 創建
XMLMapperBuilder
對象,調用其parse()
方法解析該XML映射文件
那麼接下來我們來看看XMLMapperBuilder是如何解析XML映射文件的
XMLMapperBuilder
org.apache.ibatis.builder.xml.XMLMapperBuilder
:解析XML映射文件
繼承org.apache.ibatis.builder.BaseBuilder
抽象類,該基類提供了類型轉換以及一些其他的工具方法,比較簡單,這裡就不做展述了
構造方法
public class XMLMapperBuilder extends BaseBuilder {
/**
* 基於 Java XPath 解析器
*/
private final XPathParser parser;
/**
* Mapper 構造器助手
*/
private final MapperBuilderAssistant builderAssistant;
/**
* 可被其他語句引用的可重用語句塊的集合,實際上就是 Configuration 全局配置中的 sqlFragments
*
* 例如:<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
* <sql />可能在很多地方被引用
*/
private final Map<String, XNode> sqlFragments;
/**
* 資源引用的地址,例如:com/aaa/bbb.xml
*/
private final String resource;
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
Map<String, XNode> sqlFragments, String namespace) {
this(inputStream, configuration, resource, sqlFragments);
this.builderAssistant.setCurrentNamespace(namespace);
}
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource,
Map<String, XNode> sqlFragments) {
super(configuration);
// 創建 MapperBuilderAssistant 對象
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
}
-
首先會進入
XPathParser
的構造方法,將XML映射文件解析成org.w3c.dom.Document
對象,這裡傳入了XMLMapperEntityResolver
作為解析實例對象,其中使用到本地的DTD文件 -
然後創建一個 Mapper 構造器助手
MapperBuilderAssistant
對象 -
其中一些屬性都是從Configuration全局配置對象中獲取的,例如:
typeAliasRegistry
、typeHandlerRegistry
、sqlFragments
parse方法
parse()
方法用於解析XML映射文件,在MapperAnnotationBuilder中被調用,程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
// <1> 判斷當前 Mapper 是否已經載入過
if (!configuration.isResourceLoaded(resource)) {
// <2> 解析 `<mapper />` 節點
configurationElement(parser.evalNode("/mapper"));
// <3> 標記該 Mapper 已經載入過
configuration.addLoadedResource(resource);
// <4> 綁定 Mapper
bindMapperForNamespace();
}
// <5> 解析待定的 <resultMap /> 節點
parsePendingResultMaps();
// <6> 解析待定的 <cache-ref /> 節點
parsePendingCacheRefs();
// <7> 解析待定的 SQL 語句的節點
parsePendingStatements();
}
}
<1>
根據Configuration全局配置判斷當前XML映射文件是否已經載入過,例如resource為:xxx/xxx/xxx.xml
<2>
解析 <mapper />
節點,也就是解析整個的XML映射文件,在下面的configurationElement
方法中講解
<3>
標記該XML映射文件已經載入過,往Configuration全局配置添加該欄位文件,例如添加:xxx/xxx/xxx.xml
<4>
綁定 Mapper 到該命名空間,避免在MapperAnnotationBuilder#loadXmlResource
方法中重複載入該XML映射文件
<5>
解析待定的 <resultMap />
、<cache-ref />
節點以及 Statement 對象,因為我們配置的這些對象可能還依賴的其他對象,在解析的過程中這些依賴可能還沒解析出來,導致這個對象解析失敗,所以先保存在Configuration全局配置對象中,待整個XML映射文件解析完後,再遍歷之前解析失敗的對象進行初始化,這裡就不做詳細的講述了,感興趣的小夥伴可以看一下
這裡我們來看一下configurationElement(XNode context)
方法是如何解析XML映射文件中的<mapper />
節點
configurationElement方法
configurationElement(XNode context)
方法就是來解析XML映射文件中我們定義的SQL相關資訊,程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
private void configurationElement(XNode context) {
try {
// <1> 獲得 namespace 屬性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// <2> 解析 <cache-ref /> 節點
cacheRefElement(context.evalNode("cache-ref"));
// <3> 解析 <cache /> 節點
cacheElement(context.evalNode("cache"));
// 已廢棄!老式風格的參數映射。內聯參數是首選,這個元素可能在將來被移除,這裡不會記錄。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// <4> 解析 <resultMap /> 節點
resultMapElements(context.evalNodes("/mapper/resultMap"));
// <5> 解析 <sql /> 節點們
sqlElement(context.evalNodes("/mapper/sql"));
// <6> 解析 <select /> <insert /> <update /> <delete /> 節點
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);
}
}
}
<1>
獲得 namespace
屬性,如果 XML 映射文件中定義的 namespace 和介面名稱不相等會拋出異常
<2>
解析 <cache-ref />
節點,調用cacheRefElement方法
<3>
解析 <cache />
節點,調用cacheElement方法
<4>
解析 <resultMap />
節點,調用resultMapElements方法
<5>
解析 <sql />
節點們,調用sqlElement方法
<6>
解析 <select /> <insert /> <update /> <delete />
節點,調用buildStatementFromContext方法
cacheRefElement方法
cacheRefElement(XNode context)
方法用於解析XML映射文件中的<cache-ref />
節點,程式碼如下:
private void cacheRefElement(XNode context) {
if (context != null) {
// <1> 獲得指向的 namespace 名字,並添加到 configuration 的 cacheRefMap 中
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// <2> 創建 CacheRefResolver 對象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 執行解析,獲取引用的快取對象到自己這裡
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
解析當前XML映射文件的快取配置,將當前namespace快取
引用其他的namespace的快取
形成映射關係保存在Configuration全局配置對象中
獲取引用的namespace
的快取實例,將其設置到MapperBuilderAssistant構造器助手中,在後續構建相關對象時使用
cacheElement方法
cacheElement(XNode context)
方法用於XML映射文件中的<cache />
節點,程式碼如下:
private void cacheElement(XNode context) {
if (context != null) {
// <1> 獲得負責存儲的 Cache 實現類
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// <2> 獲得負責過期的 Cache 實現類
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// <3> 獲得 flushInterval、size、readWrite、blocking 屬性
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// <4> 獲得 Properties 屬性
Properties props = context.getChildrenAsProperties();
// <5> 創建 Cache 對象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
解析該節點的相關配置,然後通過MapperBuilderAssistant
構造器小助手創建一個Cache快取實例,添加到Configuration全局配置對象中,並設置到構造器助手中,在後續構建相關對象時使用
resultMapElements方法
resultMapElements(List<XNode> list)
方法用於解析<resultMap />
節點,最後會調用
resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)
方法逐個解析生成ResultMap對象
整體的流程圖:
例如這樣配置RresultMap:
<mapper namespace="com.mybatis3.mappers.StudentMapper">
<!-- 學生 -->
<resultMap id="StudentResult" type="Student">
<result column="id" property="studentId" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="age" property="age" jdbcType="INTEGER" />
<!-- 老師 -->
<association property="teacher" javaType="Teacher">
<result column="teacher_id" property="id" jdbcType="INTEGER" />
<result column="teacher_name" property="name" jdbcType="VARCHAR" />
<result column="teacher_age" property="age" jdbcType="INTEGER" />
</association>
</resultMap>
</mapper>
resultMapElement
方法程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
// 獲取當前執行緒的上下文
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// <1> 獲得 type 屬性
String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
// 獲得 type 對應的類
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
// 從 enclosingType Class 對象獲取該 property 屬性的 Class 對象
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
// 創建 ResultMapping 集合
List<ResultMapping> resultMappings = new ArrayList<>();
// 添加父 ResultMap 的 ResultMapping 集合
resultMappings.addAll(additionalResultMappings);
// <2> 遍歷 <resultMap /> 的子節點
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) { // <2.1> 處理 <constructor /> 節點
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) { // <2.2> 處理 <discriminator /> 節點
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else { // <2.3> 處理其它節點
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 為添加該 ResultMapping 添加一個 Id 標誌
flags.add(ResultFlag.ID);
}
// 生成對應的 ResultMapping 對象
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 獲得 id 屬性,沒有的話自動生成
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
// 獲得 extends 屬性
String extend = resultMapNode.getStringAttribute("extends");
// 獲得 autoMapping 屬性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// <3> 創建 ResultMapResolver 對象,執行解析
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,
discriminator, resultMappings, autoMapping);
try {
// 處理 ResultMap 並添加到 Configuration 全局配置中
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
}
關於<resultMap />
元素的屬性配置參考MyBatis官方文檔配置說明
resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)
方法的入參分別是:
-
當前節點Node的封裝,封裝成
XNode
便於操作 -
繼承的ResultMap所對應的ResultMapping的集合,可以通過extend屬性配置繼承哪個ResultMap,沒有繼承的話就是空集合
-
所屬的ResultMap的類型,例如
<resultMap />
中的<association />
也會被解析成ResultMap,那麼它的enclosingType就是所屬ResultMap的Class對象
處理邏輯:
-
獲得 type 屬性,生成該ResultMap對應Class對象,如果沒有定義type屬性,則可能是
<association />
標籤,嘗試從所屬ResultMap的Class對象獲取property的Class對象,因為<resultMap />
標籤中配置的<association />
標籤也會解析成一個ResultMap對象 -
遍歷
<resultMap />
的子節點,依次處理- 如果是
<constructor />
節點,則調用processConstructorElement
方法進行解析,再獲取它的子節點生成對應的RequestMapping對象,這些RequestMapping對象會添加ResultFlag.CONSTRUCTOR
標記,如果是<idArg />
標籤則再添加一個ResultFlag.ID
標記,這些對象會在實例化類時,注入到構造方法中 - 如果是
<discriminator>
節點,則調用processDiscriminatorElement
方法進行解析,創建一個Discriminator選擇器對象,用於可以使用結果值來決定這個屬性使用哪個ResultMap
,基於<case />
子節點來進行映射 - 其他節點,則調用
buildResultMappingFromContext
方法進行解析,如果是<id />
則添加一個ResultFlag.ID標記,生成對應的RequestMapping對象
- 如果是
-
創建
ResultMapResolver
對象,調用其resolve()
方法執行解析,內部調用MapperBuilderAssistant
構造器小助手的addResultMap
來生成ResultMap對象的
上面的2.1
和2.2
並不複雜,感興趣的小夥伴可以查看相關方法,都已經注釋好了😈😈😈,我們來看下2.3
是如何解析成ResultMapping對象的
buildResultMappingFromContext方法
buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags)
方法將<resultMap />
標籤中的子標籤解析成RequestMapping對象,程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags)
throws Exception {
String property;
// 解析各種屬性
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
// 解析 <resultMap /> 標籤中的 <association />,<collection />,<case /> 標籤,生成 ResultMap 對象
String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.emptyList(), resultType));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// javaType 屬性
Class<?> javaTypeClass = resolveClass(javaType);
// typeHandler 屬性
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
// jdbcType 屬性
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 通過上面的屬性構建一個 ResultMapping 對象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum,
nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet,
foreignColumn, lazy);
}
}
依次從該節點中獲取相關屬性
-
這裡我們看到
nestedResultMap
的獲取,如果這個是<association />
,<collection />
或者<case />
,則會調用processNestedResultMappings
方法解析成ResultMap對象,然後返回該對象的id(沒有定義會自動生成),這樣這個RequestMapping對象就會關聯這個ResultMap對象了,這個方法內部也是調用resultMapElement
方法生成ResultMap對象的,可以回過頭再看下這個方法 -
最後通過
MapperBuilderAssistant
構造器小助手的buildResultMapping
方法根據這些屬性構建一個ResultMapping
對象並返回
整個的ResultMap對象的解析過程到這裡就結束了,關於MapperBuilderAssistant
在後續會講到,接下來我們來看看<sql />
節點的解析
sqlElement方法
sqlElement(List<XNode> list)
方法用於解析所有的<sql />
節點,內部調用sqlElement(List<XNode> list, String requiredDatabaseId)
方法,程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
// <1> 遍歷所有 <sql /> 節點
for (XNode context : list) {
// <2> 獲得 databaseId 屬性
String databaseId = context.getStringAttribute("databaseId");
// <3> 獲得完整的 id 屬性
String id = context.getStringAttribute("id");
// 設置為 `${namespace}.${id}` 格式
id = builderAssistant.applyCurrentNamespace(id, false);
// <4> 判斷 databaseId 是否匹配
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// <5> 添加到 sqlFragments 中
sqlFragments.put(id, context);
}
}
}
}
這裡僅僅是將該<sql />
節點保存至Map<String, XNode> sqlFragments
對象中(該對象保存與Configuration全局配置對象中),後續解析其他SQL語句中會使用到,例如查詢語句中使用了<include />
標籤,則需要獲取到對應的<sql />
節點將其替換
buildStatementFromContext方法
buildStatementFromContext(List<XNode> list)
方法用於解析<select /> <insert /> <update /> <delete />
節點
內部調用buildStatementFromContext(List<XNode> list, String requiredDatabaseId)
方法逐個解析生成MappedStatement
對象,程式碼如下:
public class XMLMapperBuilder extends BaseBuilder {
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// <1> 遍歷 <select /> <insert /> <update /> <delete /> 節點
for (XNode context : list) {
// <1> 創建 XMLStatementBuilder 對象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析成 MappedStatement 對象
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// <2> 解析失敗,添加到 configuration 中
configuration.addIncompleteStatement(statementParser);
}
}
}
}
為該節點創建XMLStatementBuilder
對象,然後調用其parseStatementNode()
解析成MappedStatement
對象,解析過程在下面的XMLStatementBuilder中講到
XMLStatementBuilder
org.apache.ibatis.builder.xml.XMLStatementBuilder
:解析XML映射文件中的Statement配置
也就是解析<select /> <insert /> <update /> <delete />
節點,解析過程在parseStatementNode()
方法中
構造方法
public class XMLStatementBuilder extends BaseBuilder {
private final MapperBuilderAssistant builderAssistant;
/**
* 當前 XML 節點,例如:<select />、<insert />、<update />、<delete /> 標籤
*/
private final XNode context;
/**
* 要求的 databaseId
*/
private final String requiredDatabaseId;
public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
this(configuration, builderAssistant, context, null);
}
public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context,
String databaseId) {
super(configuration);
this.builderAssistant = builderAssistant;
this.context = context;
this.requiredDatabaseId = databaseId;
}
}
parseStatementNode方法
parseStatementNode()
方法用於解析 Statement 對應節點,也就是<select /> <update /> <delete /> <insert />
節點,程式碼如下:
public class XMLStatementBuilder extends BaseBuilder {
public void parseStatementNode() {
// 獲得 id 屬性,編號。
String id = context.getStringAttribute("id");
// 獲得 databaseId , 判斷 databaseId 是否匹配
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 獲取該節點名稱
String nodeName = context.getNode().getNodeName();
// <1> 根據節點名稱判斷 SQL 類型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 是否為 Select 語句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// <2> 是否清空快取
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// <3> 是否使用快取
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// <4> 將該節點的子節點 <include /> 轉換成 <sql /> 節點
includeParser.applyIncludes(context.getNode());
// 獲取參數類型名稱
String parameterType = context.getStringAttribute("parameterType");
// <5> 參數類型名稱轉換成 Java Type
Class<?> parameterTypeClass = resolveClass(parameterType);
// <6> 獲得 lang 對應的 LanguageDriver 對象
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
// <7> 將該節點的子節點 <selectKey /> 解析成 SelectKeyGenerator 生成器
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
/*
* <8>
* 1. 如果上面存在 <selectKey /> 子節點,則獲取上面對其解析後生成的 SelectKeyGenerator
* 2. 否則判斷該節點是否配置了 useGeneratedKeys 屬性為 true 並且是 插入語句,則使用 Jdbc3KeyGenerator
*/
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE
: NoKeyGenerator.INSTANCE;
}
// <9> 創建對應的 SqlSource 對象,保存了該節點下 SQL 相關資訊
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// <10> 獲得 Statement 類型,默認 PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
// <11> 獲得返回結果類型名稱
String resultType = context.getStringAttribute("resultType");
// 獲取返回結果的 Java Type
Class<?> resultTypeClass = resolveClass(resultType);
// 獲取 resultMap
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
// 對應的 java 屬性,結合 useGeneratedKeys 使用
String keyProperty = context.getStringAttribute("keyProperty");
// 對應的 column 列名,結合 useGeneratedKeys 使用
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// <12>
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache,
resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
}
解析方法有點長,我們一步一步來看
- 根據節點名稱設置SQL語句類型,SqlCommandType:
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
- 獲取
flushCache
屬性,是否清空快取,非查詢語句默認都是true - 獲取
useCache
屬性,是否開啟快取,查詢語句默認為true - 創建
XMLIncludeTransformer
對象,調用其applyIncludes
方法將<include />
轉換成<sql />
節點,大致邏輯就是從sqlFragments
(前面已經將所有的<sql />
節點進行解析存放在其中了)獲取對應的<sql />
節點,然後替換<include />
節點,具體的轉換過程這裡就不講述了,沒有特別複雜,感興趣的小夥伴可以查看相關方法,都已經注釋好了😈😈😈 - 獲取
parameterType
屬性,參數類型,轉換成Class對象 - 獲取
lang
屬性,LanguageDriver語言驅動器,默認為XMLLanguageDriver
- 將該節點的
<selectKey />
子節點解析成 SelectKeyGenerator 生成器,用於生成一個key設置到返回對象中,在processSelectKeyNodes
方法中可以看到,該過程也會生成一個MappedStatement
對象,生成的對象的 id 為statementId+'!selectKey'
- 解析
useGeneratedKeys
屬性,獲取 SelectKeyGenerator 生成器,如果第7
步沒有生成才會進入這裡,直接返回Jdbc3KeyGenerator
單例 - 根據
XMLLanguageDriver
語言驅動創建SqlSource
對象,通過這個對象可以獲取到對應的SQL語句,在後面的《MyBatis初始化之SQL初始化》分析該創建過程 - 獲取
statementType
屬性,Statement類型,默認PREPARED
- 獲取其他的一下相關資訊,例如:
timeout
、resultMap
、keyProperty
、keyColumn
等屬性,其中配置的resultType也會轉換成ResultMap對象 - 通過
MapperBuilderAssistant
構造器小助手根據這些屬性資訊構建一個MappedStatement對象
MapperBuilderAssistant
org.apache.ibatis.builder.MapperBuilderAssistant
:Mapper構造器小助手,在前面整個XML映射文件的解析過程中,所需要創建ResultMapping、ResultMap和MappedStatement對象都是通過這個助手來創建的,那麼我們來看看它提供了哪些功能
構造方法
public class MapperBuilderAssistant extends BaseBuilder {
/**
* 當前 Mapper 命名空間
*/
private String currentNamespace;
/**
* 資源引用的地址
* 解析Mapper介面:xxx/xxx/xxx.java (best guess)
* 解析Mapper映射文件:xxx/xxx/xxx.xml
*/
private final String resource;
/**
* 當前 Cache 對象
*/
private Cache currentCache;
/**
* 是否未解析成功 Cache 引用
*/
private boolean unresolvedCacheRef; // issue #676
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
ErrorContext.instance().resource(resource);
this.resource = resource;
}
}
😈 這個小助手是為了 XMLMapperBuilder 和 MapperAnnotationBuilder 都能調用到一些公用方法
useCacheRef方法
前面在XMLMapperBuilder的cacheRefElement方法解析<cache-ref />
節點的過程中有調用這個方法,用來設置當前Cache快取實例所引用的那個對象,程式碼如下:
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true; // 標記未解決
// <1> 獲得 Cache 對象
Cache cache = configuration.getCache(namespace);
// 獲得不到,拋出 IncompleteElementException 異常
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 記錄當前 Cache 對象
currentCache = cache;
unresolvedCacheRef = false; // 標記已解決
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
入參namespace
:所引用的快取所在的namespace
根據該namespace
獲取到其快取實例對象,然後設置為當前需要使用的快取實例
useNewCache方法
前面在XMLMapperBuilder的cacheElement方法解析<cache />
節點的過程中有調用這個方法,用來創建一個Cache快取實例,程式碼如下:
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
Integer size, boolean readWrite, boolean blocking, Properties props) {
// <1> 創建 Cache 對象
// 快取實例默認為 PerpetualCache 類型,Cache 裝飾器默認為 LruCache
Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props)
.build();
// <2> 添加到 configuration 的 caches 中
configuration.addCache(cache);
// <3> 賦值給 currentCache
currentCache = cache;
return cache;
}
-
根據節點中的相關資訊通過
CacheBuilder
構造器創建一個快取實例(被裝飾的Cache實例),如何構建的程式碼有點長,這裡就不講述了,感興趣的小夥伴可以查看相關方法,都已經注釋好了😈😈😈 -
將快取實例添加到
Configuration
全局對象中 -
設置為當前需要使用的快取實例
buildResultMapping方法
前面在XMLMapperBuilder的resultMapElement方法調用的buildResultMappingFromContext方法中有調用這個方法,用來創建一個RequestMapping對象,程式碼如下:
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
boolean lazy) {
// <1> 解析對應的 Java Type
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
// 解析對應的 TypeHandler ,一般不會設置
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites;
// <2> 解析組合欄位名稱成 ResultMapping 集合,涉及「關聯的嵌套查詢」
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
// RequestMapping 關聯了子查詢,如果 column 配置了多個則一一再創建 RequestMapping 對象
composites = parseCompositeColumnName(column);
}
// <3> 創建 ResultMapping 對象
return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
.typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags)
.composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).build();
}
入參有點多,不過根據名稱可以知道其意思,大致邏輯如下:
-
解析對應的 Java Type 和 TypeHandler 的 Class 對象
-
如果嵌套的子查詢存在組合欄位,則一一解析成 ResultMapping 對象,例如需要在返回的結果集中取多個列作為嵌套查詢的入參,那麼你需要配置多個映射關係
例如子查詢的入參對象有兩個屬性,分別是name和age,而上一層查詢從資料庫返回的列名是studentName和studentAge,那麼你需要在嵌套查詢配置column屬性為:{name=studentName,age=studentAge},不然沒有映射關係無法設置子查詢的入參,這樣就會為該屬性創建兩個ResultMapping添加到
composites
集合中 -
調用
applyCurrentNamespace
方法,拼接命名空間 -
調用
parseMultipleColumnNames
方法,將字元串(以逗號分隔)解析成集合,作用: 默認情況下,在至少一個被映射到屬性的列不為空時,子對象才會被創建。 -
通過
ResultMapping.Builder
構建一個ResultMapping對象
addResultMap方法
前面在XMLMapperBuilder的resultMapElement方法使用ResultMapResolver
生成ResultMap對象時會調用這個方法,用來解析生成ResultMap對象,程式碼如下:
public ResultMap addResultMap(String id, Class<?> type, String extend, Discriminator discriminator,
List<ResultMapping> resultMappings, Boolean autoMapping) {
// <1> 獲得 ResultMap 編號,即格式為 `${namespace}.${id}`
id = applyCurrentNamespace(id, false);
// <2.1> 獲取完整的父 ResultMap 屬性,即格式為 `${namespace}.${extend}`。從這裡的邏輯來看,貌似只能獲取自己 namespace 下的 ResultMap 。
extend = applyCurrentNamespace(extend, true);
// <2.2> 如果有父類,則將父類的 ResultMap 集合,添加到 resultMappings 中。
if (extend != null) {
// <2.2.1> 獲得 extend 對應的 ResultMap 對象。如果不存在,則拋出 IncompleteElementException 異常
// 所以說 <resultMap /> 標籤如果有繼承關係就必須有先後順序?
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
// 獲取 extend 的 ResultMap 對象的 ResultMapping 集合,並移除 resultMappings
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
// 判斷當前的 resultMappings 是否有構造方法,如果有,則從 extendedResultMappings 移除所有的構造類型的 ResultMapping
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 將 extendedResultMappings 添加到 resultMappings 中
resultMappings.addAll(extendedResultMappings);
}
// <3> 創建 ResultMap 對象
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator).build();
// <4> 添加到 configuration 中
configuration.addResultMap(resultMap);
return resultMap;
}
-
調用
applyCurrentNamespace
方法拼接namespace與id,獲得ResultMap的唯一編號,格式為${namespace}.${id}
-
獲得父ResultMap的唯一編號
extend
,格式為${namespace}.${extend}
extend
為null則直接忽略- 否則獲取對應的ResultMap對象,則將
extend
的ResultMapping集合和自己的ResultMapping集合進行合併
-
通過
ResultMap.Builder
構建一個ResultMap對象,並添加到Configuration
全局配置中
addMappedStatement方法
在XMLStatementBuilder的parseStatementNode方法中會調用該方法,用來構建一個MappedStatement
對象,程式碼如下:
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap,
Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType,
boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty,
String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
// <1> 如果的指向的 Cache 未解析,拋出異常
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
// <2> 獲得 id 編號,格式為 `${namespace}.${id}`
id = applyCurrentNamespace(id, false);
// 是否為查詢語句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// <3> 創建 MappedStatement.Builder 對象
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource,
sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType)
.keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId)
.lang(lang).resultOrdered(resultOrdered).resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);
// <4> 生成 ParameterMap 對象
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// <5> 創建 MappedStatement 對象
MappedStatement statement = statementBuilder.build();
// <6> 添加到 configuration 中
configuration.addMappedStatement(statement);
return statement;
}
入參有點多,這裡就不一一進行說明了,通過其名稱大致可以知道其意思
-
如果的指向的 Cache 未解析,拋出異常
-
獲得
MappedStatement
的唯一編號id
,格式為${namespace}.${id}
-
創建
MappedStatement.Builder
對象 -
創建
ParameterMap
對象,進入getStatementParameterMap
方法可以看到,ParameterMap的Class<?> type
屬性設置為入參類型,String id
設置為statementId<parameterMap />
標籤已經被廢棄,所以這裡不會配置parameterMap
屬性 -
通過MappedStatement.Builder構建一個MappedStatement對象,並添加到Configuration全局配置中
RequestMapping
org.apache.ibatis.mapping.ResultMapping
:保存ResultMap相關子節點的資訊,也就是 Java Type 與 Jdbc Type 的映射資訊
內部定義了Builder構造器,使用構建者模式創建實例對象,有以下屬性:
public class ResultMapping {
private Configuration configuration;
/**
* Java 欄位
*/
private String property;
/**
* JDBC 列名
*/
private String column;
/**
* Java 類型
*/
private Class<?> javaType;
/**
* JDBC 類型
*/
private JdbcType jdbcType;
/**
* 類型處理器
*/
private TypeHandler<?> typeHandler;
/**
* 對應的 resultMapId
* 例如 <resultMap /> 標籤中的 <association/> 標籤會生成一個 ResultMap 對象,則這個屬性對應該 ResultMap 對象的Id
*/
private String nestedResultMapId;
/**
* 關聯的子查詢 Id
*/
private String nestedQueryId;
/**
* 不能為 null 的列名
*/
private Set<String> notNullColumns;
/**
* 列名前綴
*/
private String columnPrefix;
/**
* 具有的標記
*/
private List<ResultFlag> flags;
/**
* 關聯嵌套查詢的屬性映射
*/
private List<ResultMapping> composites;
private String resultSet;
private String foreignColumn;
private boolean lazy;
ResultMapping() {
}
}
構建過程這裡就不列出來了,可以根據注釋進行閱讀😈,最終會生成以上這些屬性
ResultMap
org.apache.ibatis.mapping.ResultMap
:保存了<resultMap />
配置的所有資訊
內部定義了Builder構造器,使用了構建者模式創建實例對象,構建的時候進行了相關屬性的分類,有以下屬性:
public class ResultMap {
/**
* Configuration 對象
*/
private Configuration configuration;
/**
* ResultMap 對象
*/
private String id;
/**
* 類型
*/
private Class<?> type;
/**
* ResultMapping 集合
*/
private List<ResultMapping> resultMappings;
/**
* ID ResultMapping 集合
*
* 當 idResultMappings 為空時,使用 {@link #resultMappings} 賦值
*/
private List<ResultMapping> idResultMappings;
/**
* 構造方法的入參 ResultMapping 集合
* 根據參數名稱已經排序好了
*
* 和 {@link #propertyResultMappings} 不存在相同元素
*/
private List<ResultMapping> constructorResultMappings;
/**
* 屬性 ResultMapping 集合
*/
private List<ResultMapping> propertyResultMappings;
/**
* 資料庫的欄位集合(全部大寫)
*/
private Set<String> mappedColumns;
/**
* Java 對象的屬性集合
*/
private Set<String> mappedProperties;
/**
* Discriminator 選擇器對象
*/
private Discriminator discriminator;
/**
* 是否有內嵌的 ResultMap
*/
private boolean hasNestedResultMaps;
/**
* 是否有嵌套關聯的子查詢
*/
private boolean hasNestedQueries;
/**
* 是否開啟自動匹配
*
* 如果設置這個屬性,MyBatis將會為這個ResultMap開啟或者關閉自動映射。這個屬性會覆蓋全局的屬性
* autoMappingBehavior。默認值為:unset。
*/
private Boolean autoMapping;
private ResultMap() {
}
}
構建過程這裡就不列出來了,可以根據注釋進行閱讀😈,最終會生成以上這些屬性
MappedStatement
org.apache.ibatis.mapping.MappedStatement
:保存了解析<select /> <update /> <delete /> <insert />
標籤內的SQL語句所生成的所有資訊
內部定義了Builder構造器,使用了構建者模式構建對象,有以下屬性:
public final class MappedStatement {
/**
* XML 映射文件路徑,例如:xxx/xxx/xxx.xml
*/
private String resource;
/**
* 全局配置對象
*/
private Configuration configuration;
/**
* 唯一編號:`${namespace}.${id}`
*/
private String id;
/**
* 這是一個給驅動的建議值,嘗試讓驅動程式每次批量返回的結果行數等於這個設置值
* 默認值為未設置(unset)(依賴驅動)
*/
private Integer fetchSize;
/**
* 這個設置是在拋出異常之前,驅動程式等待資料庫返回請求結果的秒數
* 默認值為未設置(unset)(依賴資料庫驅動)
*/
private Integer timeout;
/**
* Statement 的類型:STATEMENT PREPARED CALLABLE,默認 PREPARED
* 分別對應:Statement PreparedStatement CallableStatement
*/
private StatementType statementType;
private ResultSetType resultSetType;
/**
* SQL 相關資訊
*/
private SqlSource sqlSource;
/**
* 快取對象
*/
private Cache cache;
private ParameterMap parameterMap;
/**
* ResultMap對象
* 配置多個時需要加上 namespace 並以逗號分隔
*/
private List<ResultMap> resultMaps;
/**
* 是否清空快取
*/
private boolean flushCacheRequired;
/**
* 是否使用快取
*/
private boolean useCache;
/**
* 這個設置僅針對嵌套結果 select 語句,默認值:false
* 如果為 true,將會假設包含了嵌套結果集或是分組,當返回一個主結果行時,就不會產生對前面結果集的引用
* 這就使得在獲取嵌套結果集的時候不至於記憶體不夠用
*/
private boolean resultOrdered;
/**
* SQL 語句類型
*/
private SqlCommandType sqlCommandType;
/**
* key 的生成器
*/
private KeyGenerator keyGenerator;
/**
* key 的生成器的 Java 屬性
*/
private String[] keyProperties;
/**
* key 的生成器的 column 列名
*/
private String[] keyColumns;
/**
* 是否有內嵌的 ResultMap
*/
private boolean hasNestedResultMaps;
/**
* 資料庫表示
*/
private String databaseId;
/**
* 日誌對象
*/
private Log statementLog;
/**
* 語言驅動,默認為XMLLanguageDriver
*/
private LanguageDriver lang;
/**
* 這個設置僅適用於多結果集的情況
* 它將列出語句執行後返回的結果集並賦予每個結果集一個名稱,多個名稱之間以逗號分隔
*/
private String[] resultSets;
MappedStatement() {
// constructor disabled
}
}
構建過程這裡就不列出來了,可以根據注釋進行閱讀😈,最終會生成以上這些屬性
其中SqlSource
是通過XMLLanguageDriver
語言驅動創建的,可以回到XmlStatementBuilder的parseStatementNode()方法看看,在後面的《MyBatis初始化之SQL初始化》分析整個創建過程
總結
本分分析了MyBatis在初始化時載入Mapper介面與XML映射文件的整個過程
-
在
XMLConfigBuilder
中將用戶配置的Mapper介面所在包路徑package
添加到MapperRegistry
註冊表中 -
在
MapperRegistry
註冊表中會對包下的所有Mapper介面進行解析,每個介面都會創建對應的MapperProxyFactory
動態代理對象工廠,並保存,也會通過MapperAnnotationBuilder
對該介面進行解析,解析過程:- 首先通過該Mapper介面的名稱獲取對應的XML映射文件,獲取到該文件資源進行載入解析,解析後的對象都會跟XML映射文件中配置的
namespace
屬性關聯,所以XML映射文件的名稱要與Mapper介面的名稱保持一致,配置的namespace
屬性要與介面的全名保持一致 - 然後解析Mapper介面的MyBatis相關註解
- 首先通過該Mapper介面的名稱獲取對應的XML映射文件,獲取到該文件資源進行載入解析,解析後的對象都會跟XML映射文件中配置的
-
解析XML映射文件的過程中是在
XMLMapperBuilder
中進行的,會使用到MapperBuilderAssistant
小助手用於創建ResultMapping
、ResultMap
和MappedStatement
對象 -
其中解析
<select /> <update /> <delete /> <insert />
標籤的解析過程又在XMLStatementBuilder
對象中進行
在XMLStatementBuilder
解析上面標籤的時候需要通過XMLLanguageDriver
語言驅動創建SqlSource
對象,這個過程涉及到的篇幅有點多,會在接下來的《MyBatis初始化(三)之SQL初始化(上)》中開始分析
最終所有的MyBatis配置、Mapper介面和XML映射文件生成的相應對象都保存在了Configuration全局配置對象中,那麼接下來我們來看看SQL語句在MyBatis中是如何初始化的
參考文章:芋道源碼《精盡 MyBatis 源碼分析》