概述
前面幾篇我們介紹了MyBatis中配置檔案的解析過程。今天我們接着來看看MyBatis的另外一個核心知識點—映射檔案的解析。本文将重點介紹
<cache>
節點和
<cache-ref>
的解析。
前置說明
Mapper 映射檔案的解析是從XMLConfigBuilder類的對mappers 節點解析開始。mappers節點的配置有很多形式,如下圖所示:
<!-- 映射器 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接口,并根據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);
//映射器比較複雜,調用XMLMapperBuilder
//注意在for循環裡每個mapper都重新new一個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);
//映射器比較複雜,調用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);
//直接把這個映射加入配置
configuration.addMapper(mapperInterface);
} else {
// 以上條件都不滿足,則抛出異常
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
上述解析方法的主要流程如下流程圖所示:
如上流程圖,mappers節點的解析還是比較複雜的,這裡我挑幾個部分說下。其中
-
還是利用ResolverUtil找出包下所有的類,然後循環調用MapperRegistry類的addMapper方法。待會我們在分析這個方法configuration.addMappers(mapperPackage)
-
配置resource或者url的都需要先建立一個XMLMapperBuilder對象。然後調用XMLMapperBuilder的parse方法。
首先我們來分析第一部分。
注冊Mapper
//* MapperRegistry 添加映射的方法
public <T> void addMapper(Class<T> type) {
//mapper必須是接口!才會添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重複添加了,報錯
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 将映射器的class對象,以及其代理類設定到集合中,采用的是JDK代理
knownMappers.put(type, new MapperProxyFactory<T>(type));
//在運作解析器之前添加類型是很重要的,否則,可能會自動嘗試綁定映射器解析器。如果類型已經知道,則不會嘗試。
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加載過程中出現異常需要再将這個mapper從mybatis中删除,
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
//* MapperProxyFactory
protected T newInstance(MapperProxy<T> mapperProxy) {
//用JDK自帶的動态代理生成映射器
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
如上,addMapper方法主要有如下流程:
- 判斷mapper是否是接口,是否已經添加,如果不滿足條件則直接抛出異常
- 将mapper接口的class對象及其代理類添加到集合彙總
- 建立
對象,主要是添加一些中繼資料,如Select.classMapperAnnotationBuilder
-
調用MapperAnnotationBuilder類的parse方法進行最終的解析
其中第4步驟相對而言比較複雜,待會我在分析。接着我們來分析第二部分
解析mapper
就像剛剛我們提到的解析mapper的parse方法有兩個,一個是XMLMapperBuilder的parse方法,一個是MapperAnnotationBuilder的parse方法。接下來我分别分析下。
//* XMLMapperBuilder
public void parse() {
//如果沒有加載過再加載,防止重複加載
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//添加資源路徑到"已解析資源集合"中
configuration.addLoadedResource(resource);
//綁定映射器到namespace
bindMapperForNamespace();
}
//處理未完成解析的節點
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
如上,解析的流程主要有以下四個:
- 配置mapper
- 添加資源路徑到"已解析資源集合"中
- 綁定映射器到namespace
-
處理未完成解析的節點。
其中第一步配置mapper中又包含了cache,resultMap等節點的解析,是我們重點分析的部分。第二,第三步比較簡單,在此就不分析了。第四步一會做簡要分析。
接下來我們在看看MapperAnnotationBuilder的parse方法,該類主要是以注解的方式建構mapper。有的比較少。
public void parse() {
String resource = type.toString();
//如果沒有加載過再加載,防止重複加載
if (!configuration.isResourceLoaded(resource)) {
//加載映射檔案,内部邏輯有建立XMLMapperBuilder對象,并調用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方法邏輯上略有不同,主要展現在對節點的解析上。接下來我們再來看看cache的配置以及節點的解析。
配置cache
如下,一個簡單的cache配置,說明,預設情況下,MyBatis隻啟用了本地的會話緩存,它僅僅針對一個繪畫中的資料進行緩存,要啟動全局的二級緩存隻需要在你的sql映射檔案中添加一行:
<cache/>
或者設定手動設定一些值,如下:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
如上配置的意思是:
- 按先進先出的政策淘汰緩存項
- 緩存的容量為512個對象引用
- 緩存每隔60秒重新整理一次
-
緩存傳回的對象是寫安全的,即在外部修改對象不會影響到緩存内部存儲對象
這個簡單語句的效果如下:
- 映射語句檔案中的所有 select 語句的結果将會被緩存。
- 映射語句檔案中的所有 insert、update 和 delete 語句會重新整理緩存。
- 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
- 緩存不會定時進行重新整理(也就是說,沒有重新整理間隔)。
- 緩存會儲存清單或對象(無論查詢方法傳回哪種)的 1024 個引用。
- 緩存會被視為讀/寫緩存,這意味着擷取到的對象并不是共享的,可以安全地被調用者修改,而不幹擾其他調用者或線程所做的潛在修改。
cache 節點的解析
cache節點的解析入口是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);
//讀入額外的配置資訊,易于第三方的緩存擴充,例:
// <cache type="com.domain.something.MyCustomCache">
// <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
// </cache>
Properties props = context.getChildrenAsProperties();
//調用builderAssistant.useNewCache
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
如上,前面主要是一些設定,沒啥好說的, 我們主要看看調用builderAssistant.useNewCache 設定緩存資訊的方法。MapperBuilderAssistant是一個映射建構器助手。
設定緩存資訊useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//這裡面又判斷了一下是否為null就用預設值,有點和XMLMapperBuilder.cacheElement邏輯重複了
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
//調用CacheBuilder建構cache,id=currentNamespace(使用建造者模式建構緩存執行個體)
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//添加緩存到Configuration對象中
configuration.addCache(cache);
//設定currentCache周遊,即目前使用的緩存
currentCache = cache;
return cache;
}
如上,useNewCache 方法的主要有如下邏輯:
- 調用CacheBuilder建構cache,id=currentNamespace(使用建造者模式建構緩存執行個體)
- 添加緩存到Configuration對象中
-
設定currentCache周遊,即目前使用的緩存
這裡,我們主要介紹下第一步通過CacheBuilder建構cache的過程,該過程運用了建造者模式。
建構cache
public Cache build() {
// 1. 設定預設的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
setDefaultImplementations();
//通過反射建立緩存
Cache cache = newBaseCacheInstance(implementation, id);
//設額外屬性,初始化Cache對象
setCacheProperties(cache);
// 2. 僅對内置緩存PerpetualCache應用裝飾器
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
//裝飾者模式一個個包裝cache
cache = newCacheDecoratorInstance(decorator, cache);
//又要來一遍設額外屬性
setCacheProperties(cache);
}
//3. 應用标準的裝飾者,比如LoggingCache,SynchronizedCache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
//4.如果是custom緩存,且不是日志,要加日志
cache = new LoggingCache(cache);
}
return cache;
}
如上,該建構緩存的方法主要流程有:
- 設定預設的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
- 通過反射建立緩存
- 設定額外屬性,初始化Cache對象
- 裝飾者模式一個個包裝cache,僅針對内置緩存PerpetualCache應用裝飾器
- 應用标準的裝飾者,比如LoggingCache,SynchronizedCache
-
如果是custom緩存,且不是日志,要加日志
這裡,我将重點介紹第三步和第五步。其餘步驟相對比較簡單,再次不做過多的分析。
設定額外屬性
private void setCacheProperties(Cache cache) {
if (properties != null) {
// 為緩存執行個體生成一個"元資訊"執行個體,forObject方法調用層次比較深,
// 但最終調用了MetaClass的forClass方法
MetaObject metaCache = SystemMetaObject.forObject(cache);
//用反射設定額外的property屬性
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
//檢測cache是否有該屬性對應的setter方法
if (metaCache.hasSetter(name)) {
// 擷取setter方法的參數類型
Class<?> type = metaCache.getSetterType(name);
//根據參數類型對屬性值進行轉換,并将轉換後的值
// 通過setter方法設定到Cache執行個體中。
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
/*
* 此處及以下分支包含兩個步驟:
* 1. 類型裝換 ->Integer.valueOf(value)
* 2. 将轉換後的值設定到緩存執行個體中->
* metaCache.setValue(name,value)
*/
metaCache.setValue(name, Integer.valueOf(value));
//省略其餘設值代碼
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
}
如上是設定額外屬性的方法,方法的注釋比較詳實,再次不在贅述。下面我們來看看第五步。
應用标準裝飾者
private Cache setStandardDecorators(Cache cache) {
try {
// 建立"元資訊"對象
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,可讀寫的緩存 會傳回緩存對象的拷貝(通過序列化) 。這會慢一些,但是安全,是以預設是 false。
cache = new SerializedCache(cache);
}
//日志緩存
cache = new LoggingCache(cache);
//同步緩存, 3.2.6以後這個類已經沒用了,考慮到Hazelcast, EhCache已經有鎖機制了,是以這個鎖就畫蛇添足了。
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);
}
}