天天看點

MyBatis 學習筆記(六)---源碼分析篇---映射檔案的解析過程(一)

概述

前面幾篇我們介紹了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.");
          }
        }
      }
    }      

上述解析方法的主要流程如下流程圖所示:

MyBatis 學習筆記(六)---源碼分析篇---映射檔案的解析過程(一)

如上流程圖,mappers節點的解析還是比較複雜的,這裡我挑幾個部分說下。其中

  1. ​configuration.addMappers(mapperPackage)​

    ​還是利用ResolverUtil找出包下所有的類,然後循環調用MapperRegistry類的addMapper方法。待會我們在分析這個方法
  2. 配置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方法主要有如下流程:

  1. 判斷mapper是否是接口,是否已經添加,如果不滿足條件則直接抛出異常
  2. 将mapper接口的class對象及其代理類添加到集合彙總
  3. 建立​

    ​MapperAnnotationBuilder​

    ​對象,主要是添加一些中繼資料,如Select.class
  4. 調用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();
  }      

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

  1. 配置mapper
  2. 添加資源路徑到"已解析資源集合"中
  3. 綁定映射器到namespace
  4. 處理未完成解析的節點。

    其中第一步配置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"/>      

如上配置的意思是:

  1. 按先進先出的政策淘汰緩存項
  2. 緩存的容量為512個對象引用
  3. 緩存每隔60秒重新整理一次
  4. 緩存傳回的對象是寫安全的,即在外部修改對象不會影響到緩存内部存儲對象

    這個簡單語句的效果如下:

  • 映射語句檔案中的所有 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 方法的主要有如下邏輯:

  1. 調用CacheBuilder建構cache,id=currentNamespace(使用建造者模式建構緩存執行個體)
  2. 添加緩存到Configuration對象中
  3. 設定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;
  }      

如上,該建構緩存的方法主要流程有:

  1. 設定預設的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
  2. 通過反射建立緩存
  3. 設定額外屬性,初始化Cache對象
  4. 裝飾者模式一個個包裝cache,僅針對内置緩存PerpetualCache應用裝飾器
  5. 應用标準的裝飾者,比如LoggingCache,SynchronizedCache
  6. 如果是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);
    }
  }      

總結

源碼位址

繼續閱讀