MyBatis 學(xué)習(xí)筆記(六)---源碼分析篇---映射文件的解析過程(一)
概述
前面幾篇我們介紹了MyBatis中配置文件的解析過程。今天我們接著來看看MyBatis的另外一個(gè)核心知識點(diǎn)—映射文件的解析。本文將重點(diǎn)介紹<cache>
節(jié)點(diǎn)和<cache-ref>
的解析。
前置說明
Mapper 映射文件的解析是從XMLConfigBuilder類的對mappers 節(jié)點(diǎn)解析開始。mappers節(jié)點(diǎn)的配置有很多形式,如下圖所示:
<!-- 映射器 10.1使用類路徑-->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 10.2使用絕對url路徑-->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 10.3使用java類名-->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 10.4自動掃描包下所有映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
mappers的解析入口方法
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自動掃描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
// 從指定的包中查找mapper接口,并根據(jù)mapper接口解析映射配置
configuration.addMappers(mapperPackage);
} else {
// 獲取resource/url/class等屬性
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//resource 不為空,且其他兩者為空,則從指定路徑中加載配置
if (resource != null && url == null && mapperClass == null) {
//10.1使用類路徑
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比較復(fù)雜,調(diào)用XMLMapperBuilder
//注意在for循環(huán)里每個(gè)mapper都重新new一個(gè)XMLMapperBuilder,來解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用絕對url路徑
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比較復(fù)雜,調(diào)用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java類名
Class<?> mapperInterface = Resources.classForName(mapperClass);
//直接把這個(gè)映射加入配置
configuration.addMapper(mapperInterface);
} else {
// 以上條件都不滿足,則拋出異常
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
上述解析方法的主要流程如下流程圖所示:
如上流程圖,mappers節(jié)點(diǎn)的解析還是比較復(fù)雜的,這里我挑幾個(gè)部分說下。其中
configuration.addMappers(mapperPackage)
還是利用ResolverUtil找出包下所有的類,然后循環(huán)調(diào)用MapperRegistry類的addMapper方法。待會我們在分析這個(gè)方法- 配置resource或者url的都需要先創(chuàng)建一個(gè)XMLMapperBuilder對象。然后調(diào)用XMLMapperBuilder的parse方法。
首先我們來分析第一部分。
注冊Mapper
//* MapperRegistry 添加映射的方法
public <T> void addMapper(Class<T> type) {
//mapper必須是接口!才會添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重復(fù)添加了,報(bào)錯(cuò)
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 將映射器的class對象,以及其代理類設(shè)置到集合中,采用的是JDK代理
knownMappers.put(type, new MapperProxyFactory<T>(type));
//在運(yùn)行解析器之前添加類型是很重要的,否則,可能會自動嘗試綁定映射器解析器。如果類型已經(jīng)知道,則不會嘗試。
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加載過程中出現(xiàn)異常需要再將這個(gè)mapper從mybatis中刪除,
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
//* MapperProxyFactory
protected T newInstance(MapperProxy<T> mapperProxy) {
//用JDK自帶的動態(tài)代理生成映射器
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
如上,addMapper方法主要有如下流程:
- 判斷mapper是否是接口,是否已經(jīng)添加,如果不滿足條件則直接拋出異常
- 將mapper接口的class對象及其代理類添加到集合匯總
- 創(chuàng)建
MapperAnnotationBuilder
對象,主要是添加一些元數(shù)據(jù),如Select.class - 調(diào)用MapperAnnotationBuilder類的parse方法進(jìn)行最終的解析
其中第4步驟相對而言比較復(fù)雜,待會我在分析。接著我們來分析第二部分
解析mapper
就像剛剛我們提到的解析mapper的parse方法有兩個(gè),一個(gè)是XMLMapperBuilder的parse方法,一個(gè)是MapperAnnotationBuilder的parse方法。接下來我分別分析下。
//* XMLMapperBuilder
public void parse() {
//如果沒有加載過再加載,防止重復(fù)加載
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//添加資源路徑到"已解析資源集合"中
configuration.addLoadedResource(resource);
//綁定映射器到namespace
bindMapperForNamespace();
}
//處理未完成解析的節(jié)點(diǎn)
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
如上,解析的流程主要有以下四個(gè):
- 配置mapper
- 添加資源路徑到"已解析資源集合"中
- 綁定映射器到namespace
- 處理未完成解析的節(jié)點(diǎn)。
其中第一步配置mapper中又包含了cache,resultMap等節(jié)點(diǎn)的解析,是我們重點(diǎn)分析的部分。第二,第三步比較簡單,在此就不分析了。第四步一會做簡要分析。
接下來我們在看看MapperAnnotationBuilder的parse方法,該類主要是以注解的方式構(gòu)建mapper。有的比較少。
public void parse() {
String resource = type.toString();
//如果沒有加載過再加載,防止重復(fù)加載
if (!configuration.isResourceLoaded(resource)) {
//加載映射文件,內(nèi)部邏輯有創(chuàng)建XMLMapperBuilder對象,并調(diào)用parse方法。
loadXmlResource();
//添加資源路徑到"已解析資源集合"中
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
//解析cache
parseCache();
//解析cacheRef
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
//解析sql,ResultMap
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
如上,MapperAnnotationBuilder的parse方法與XMLMapperBuilder的parse方法邏輯上略有不同,主要體現(xiàn)在對節(jié)點(diǎn)的解析上。接下來我們再來看看cache的配置以及節(jié)點(diǎn)的解析。
配置cache
如下,一個(gè)簡單的cache配置,說明,默認(rèn)情況下,MyBatis只啟用了本地的會話緩存,它僅僅針對一個(gè)繪畫中的數(shù)據(jù)進(jìn)行緩存,要啟動全局的二級緩存只需要在你的sql映射文件中添加一行:
<cache/>
或者設(shè)置手動設(shè)置一些值,如下:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
如上配置的意思是:
- 按先進(jìn)先出的策略淘汰緩存項(xiàng)
- 緩存的容量為512個(gè)對象引用
- 緩存每隔60秒刷新一次
- 緩存返回的對象是寫安全的,即在外部修改對象不會影響到緩存內(nèi)部存儲對象
這個(gè)簡單語句的效果如下:
- 映射語句文件中的所有 select 語句的結(jié)果將會被緩存。
- 映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
- 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
- 緩存不會定時(shí)進(jìn)行刷新(也就是說,沒有刷新間隔)。
- 緩存會保存列表或?qū)ο螅o論查詢方法返回哪種)的 1024 個(gè)引用。
- 緩存會被視為讀/寫緩存,這意味著獲取到的對象并不是共享的,可以安全地被調(diào)用者修改,而不干擾其他調(diào)用者或線程所做的潛在修改。
cache 節(jié)點(diǎn)的解析
cache節(jié)點(diǎn)的解析入口是XMLMapperBuilder類的configurationElement方法。我們直接來看看具體解析cache的方法。
//* XMLMapperBuilder
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
//讀入額外的配置信息,易于第三方的緩存擴(kuò)展,例:
// <cache type="com.domain.something.MyCustomCache">
// <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
// </cache>
Properties props = context.getChildrenAsProperties();
//調(diào)用builderAssistant.useNewCache
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
如上,前面主要是一些設(shè)置,沒啥好說的, 我們主要看看調(diào)用builderAssistant.useNewCache 設(shè)置緩存信息的方法。MapperBuilderAssistant是一個(gè)映射構(gòu)建器助手。
設(shè)置緩存信息useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//這里面又判斷了一下是否為null就用默認(rèn)值,有點(diǎn)和XMLMapperBuilder.cacheElement邏輯重復(fù)了
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
//調(diào)用CacheBuilder構(gòu)建cache,id=currentNamespace(使用建造者模式構(gòu)建緩存實(shí)例)
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//添加緩存到Configuration對象中
configuration.addCache(cache);
//設(shè)置currentCache遍歷,即當(dāng)前使用的緩存
currentCache = cache;
return cache;
}
如上,useNewCache 方法的主要有如下邏輯:
- 調(diào)用CacheBuilder構(gòu)建cache,id=currentNamespace(使用建造者模式構(gòu)建緩存實(shí)例)
- 添加緩存到Configuration對象中
- 設(shè)置currentCache遍歷,即當(dāng)前使用的緩存
這里,我們主要介紹下第一步通過CacheBuilder構(gòu)建cache的過程,該過程運(yùn)用了建造者模式。
構(gòu)建cache
public Cache build() {
// 1. 設(shè)置默認(rèn)的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
setDefaultImplementations();
//通過反射創(chuàng)建緩存
Cache cache = newBaseCacheInstance(implementation, id);
//設(shè)額外屬性,初始化Cache對象
setCacheProperties(cache);
// 2. 僅對內(nèi)置緩存PerpetualCache應(yīng)用裝飾器
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
//裝飾者模式一個(gè)個(gè)包裝cache
cache = newCacheDecoratorInstance(decorator, cache);
//又要來一遍設(shè)額外屬性
setCacheProperties(cache);
}
//3. 應(yīng)用標(biāo)準(zhǔn)的裝飾者,比如LoggingCache,SynchronizedCache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
//4.如果是custom緩存,且不是日志,要加日志
cache = new LoggingCache(cache);
}
return cache;
}
如上,該構(gòu)建緩存的方法主要流程有:
- 設(shè)置默認(rèn)的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
- 通過反射創(chuàng)建緩存
- 設(shè)置額外屬性,初始化Cache對象
- 裝飾者模式一個(gè)個(gè)包裝cache,僅針對內(nèi)置緩存PerpetualCache應(yīng)用裝飾器
- 應(yīng)用標(biāo)準(zhǔn)的裝飾者,比如LoggingCache,SynchronizedCache
- 如果是custom緩存,且不是日志,要加日志
這里,我將重點(diǎn)介紹第三步和第五步。其余步驟相對比較簡單,再次不做過多的分析。
設(shè)置額外屬性
private void setCacheProperties(Cache cache) {
if (properties != null) {
// 為緩存實(shí)例生成一個(gè)"元信息"實(shí)例,forObject方法調(diào)用層次比較深,
// 但最終調(diào)用了MetaClass的forClass方法
MetaObject metaCache = SystemMetaObject.forObject(cache);
//用反射設(shè)置額外的property屬性
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
//檢測cache是否有該屬性對應(yīng)的setter方法
if (metaCache.hasSetter(name)) {
// 獲取setter方法的參數(shù)類型
Class<?> type = metaCache.getSetterType(name);
//根據(jù)參數(shù)類型對屬性值進(jìn)行轉(zhuǎn)換,并將轉(zhuǎn)換后的值
// 通過setter方法設(shè)置到Cache實(shí)例中。
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
/*
* 此處及以下分支包含兩個(gè)步驟:
* 1. 類型裝換 ->Integer.valueOf(value)
* 2. 將轉(zhuǎn)換后的值設(shè)置到緩存實(shí)例中->
* metaCache.setValue(name,value)
*/
metaCache.setValue(name, Integer.valueOf(value));
//省略其余設(shè)值代碼
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
}
如上是設(shè)置額外屬性的方法,方法的注釋比較詳實(shí),再次不在贅述。下面我們來看看第五步。
應(yīng)用標(biāo)準(zhǔn)裝飾者
private Cache setStandardDecorators(Cache cache) {
try {
// 創(chuàng)建"元信息"對象
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
//刷新緩存間隔,怎么刷新呢,用ScheduledCache來刷,還是裝飾者模式,漂亮!
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
//如果readOnly=false,可讀寫的緩存 會返回緩存對象的拷貝(通過序列化) 。這會慢一些,但是安全,因此默認(rèn)是 false。
cache = new SerializedCache(cache);
}
//日志緩存
cache = new LoggingCache(cache);
//同步緩存, 3.2.6以后這個(gè)類已經(jīng)沒用了,考慮到Hazelcast, EhCache已經(jīng)有鎖機(jī)制了,所以這個(gè)鎖就畫蛇添足了。
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
總結(jié)
本文 按照代碼運(yùn)行的脈絡(luò),先是介紹了mappers節(jié)點(diǎn)的解析,然后概括了映射文件的解析,最后重點(diǎn)介紹了cache 節(jié)點(diǎn)的解析。
源碼地址
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥