MyBatis 學(xué)習(xí)筆記(六)---源碼分析篇---映射文件的解析過程(一)

概述

前面幾篇我們介紹了MyBatis中配置文件的解析過程。今天我們接著來看看MyBatis的另外一個核心知識點—映射文件的解析。本文將重點介紹<cache>節(jié)點和<cache-ref>的解析。

前置說明

Mapper 映射文件的解析是從XMLConfigBuilder類的對mappers 節(jié)點解析開始。mappers節(jié)點的配置有很多形式,如下圖所示:

 <!-- 映射器 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)里每個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);
            //映射器比較復(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);
            //直接把這個映射加入配置
            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é)點的解析還是比較復(fù)雜的,這里我挑幾個部分說下。其中

  1. configuration.addMappers(mapperPackage)還是利用ResolverUtil找出包下所有的類,然后循環(huán)調(diào)用MapperRegistry類的addMapper方法。待會我們在分析這個方法
  2. 配置resource或者url的都需要先創(chuàng)建一個XMLMapperBuilder對象。然后調(diào)用XMLMapperBuilder的parse方法。
    首先我們來分析第一部分。

注冊Mapper

//* MapperRegistry 添加映射的方法
public <T> void addMapper(Class<T> type) {
    //mapper必須是接口!才會添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重復(fù)添加了,報錯
        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));
		//在運行解析器之前添加類型是很重要的,否則,可能會自動嘗試綁定映射器解析器。如果類型已經(jīng)知道,則不會嘗試。		
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加載過程中出現(xiàn)異常需要再將這個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方法主要有如下流程:

  1. 判斷mapper是否是接口,是否已經(jīng)添加,如果不滿足條件則直接拋出異常
  2. 將mapper接口的class對象及其代理類添加到集合匯總
  3. 創(chuàng)建MapperAnnotationBuilder對象,主要是添加一些元數(shù)據(jù),如Select.class
  4. 調(diào)用MapperAnnotationBuilder類的parse方法進行最終的解析
    其中第4步驟相對而言比較復(fù)雜,待會我在分析。接著我們來分析第二部分

解析mapper

就像剛剛我們提到的解析mapper的parse方法有兩個,一個是XMLMapperBuilder的parse方法,一個是MapperAnnotationBuilder的parse方法。接下來我分別分析下。

//* XMLMapperBuilder 
  public void parse() {
    //如果沒有加載過再加載,防止重復(fù)加載
    if (!configuration.isResourceLoaded(resource)) {
      //配置mapper
      configurationElement(parser.evalNode("/mapper"));
      //添加資源路徑到"已解析資源集合"中
      configuration.addLoadedResource(resource);
      //綁定映射器到namespace
      bindMapperForNamespace();
    }

    //處理未完成解析的節(jié)點
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }

如上,解析的流程主要有以下四個:

  1. 配置mapper
  2. 添加資源路徑到"已解析資源集合"中
  3. 綁定映射器到namespace
  4. 處理未完成解析的節(jié)點。
    其中第一步配置mapper中又包含了cache,resultMap等節(jié)點的解析,是我們重點分析的部分。第二,第三步比較簡單,在此就不分析了。第四步一會做簡要分析。
    接下來我們在看看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é)點的解析上。接下來我們再來看看cache的配置以及節(jié)點的解析。

配置cache

如下,一個簡單的cache配置,說明,默認(rèn)情況下,MyBatis只啟用了本地的會話緩存,它僅僅針對一個繪畫中的數(shù)據(jù)進行緩存,要啟動全局的二級緩存只需要在你的sql映射文件中添加一行:

<cache/>

或者設(shè)置手動設(shè)置一些值,如下:

  <cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

如上配置的意思是:

  1. 按先進先出的策略淘汰緩存項
  2. 緩存的容量為512個對象引用
  3. 緩存每隔60秒刷新一次
  4. 緩存返回的對象是寫安全的,即在外部修改對象不會影響到緩存內(nèi)部存儲對象
    這個簡單語句的效果如下:
  • 映射語句文件中的所有 select 語句的結(jié)果將會被緩存。
  • 映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
  • 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
  • 緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
  • 緩存會保存列表或?qū)ο螅o論查詢方法返回哪種)的 1024 個引用。
  • 緩存會被視為讀/寫緩存,這意味著獲取到的對象并不是共享的,可以安全地被調(diào)用者修改,而不干擾其他調(diào)用者或線程所做的潛在修改。

cache 節(jié)點的解析

cache節(jié)點的解析入口是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();
      //調(diào)用builderAssistant.useNewCache
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

如上,前面主要是一些設(shè)置,沒啥好說的, 我們主要看看調(diào)用builderAssistant.useNewCache 設(shè)置緩存信息的方法。MapperBuilderAssistant是一個映射構(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)值,有點和XMLMapperBuilder.cacheElement邏輯重復(fù)了
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    //調(diào)用CacheBuilder構(gòu)建cache,id=currentNamespace(使用建造者模式構(gòu)建緩存實例)
    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 方法的主要有如下邏輯:

  1. 調(diào)用CacheBuilder構(gòu)建cache,id=currentNamespace(使用建造者模式構(gòu)建緩存實例)
  2. 添加緩存到Configuration對象中
  3. 設(shè)置currentCache遍歷,即當(dāng)前使用的緩存
    這里,我們主要介紹下第一步通過CacheBuilder構(gòu)建cache的過程,該過程運用了建造者模式。

構(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) {
          //裝飾者模式一個個包裝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)建緩存的方法主要流程有:

  1. 設(shè)置默認(rèn)的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
  2. 通過反射創(chuàng)建緩存
  3. 設(shè)置額外屬性,初始化Cache對象
  4. 裝飾者模式一個個包裝cache,僅針對內(nèi)置緩存PerpetualCache應(yīng)用裝飾器
  5. 應(yīng)用標(biāo)準(zhǔn)的裝飾者,比如LoggingCache,SynchronizedCache
  6. 如果是custom緩存,且不是日志,要加日志
    這里,我將重點介紹第三步和第五步。其余步驟相對比較簡單,再次不做過多的分析。

設(shè)置額外屬性

 private void setCacheProperties(Cache cache) {
    if (properties != null) {
//      為緩存實例生成一個"元信息"實例,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ù)類型對屬性值進行轉(zhuǎn)換,并將轉(zhuǎn)換后的值
//          通過setter方法設(shè)置到Cache實例中。
          if (String.class == type) {
            metaCache.setValue(name, value);
          } else if (int.class == type
              || Integer.class == type) {
            /*
             * 此處及以下分支包含兩個步驟:
             * 1. 類型裝換 ->Integer.valueOf(value)
             * 2. 將轉(zhuǎn)換后的值設(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è)置額外屬性的方法,方法的注釋比較詳實,再次不在贅述。下面我們來看看第五步。

應(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以后這個類已經(jīng)沒用了,考慮到Hazelcast, EhCache已經(jīng)有鎖機制了,所以這個鎖就畫蛇添足了。
      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é)

本文 按照代碼運行的脈絡(luò),先是介紹了mappers節(jié)點的解析,然后概括了映射文件的解析,最后重點介紹了cache 節(jié)點的解析。

源碼地址

源代碼地址




作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥