天天看點

MyBatis源碼解析(四) --- 解析Mapper節點

映射檔案包含多種二級節點,比如 < cache>,< resultMap>,< sql>以及 <select|insert|update|delete> 等。除此之外,還包含了一些三級節點,比如 < include>,< if>, < where> 等。這些節點的解析過程将會在接下來的内容中陸續進行分析。在分析之前,我們 先來看一個映射檔案配置示例。

<mapper namespace="xyz.coolblog.dao.AuthorDao">

    <cache/>

    <resultMap id="authorResult" type="Author">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!-- ... -->
    </resultMap>

    <sql id="table">
        author
    </sql>

    <select id="findOne" resultMap="authorResult">
        SELECT
            id, name, age, sex, email
        FROM
            <include refid="table"/>
        WHERE
            id = #{id}
    </select>

    <!-- <insert|update|delete/> -->
</mapper>
           

上面是一個比較簡單的映射檔案,還有一些的節點未出現在上面。以上配置中每種節點 的解析邏輯都封裝在了相應的方法中,這些方法由 XMLMapperBuilder 類的 configurationElement 方法統一調用。該方法的邏輯如下:

private void configurationElement(XNode context) {
    try {
    // 擷取 mapper 命名空間
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 設定命名空間到 builderAssistant 中
      builderAssistant.setCurrentNamespace(namespace);
      // 解析 <cache-ref> 節點
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析 <cache> 節點
      cacheElement(context.evalNode("cache"));
      // 已廢棄配置,這裡不做分析
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析 <resultMap> 節點
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析 <sql> 節點
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析 <select>、...、<delete> 等節點
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }
           

解析< cache>節點

MyBatis 提供了一、二級緩存,其中一級緩存是 SqlSession 級别的,預設為開啟狀态。 二級緩存配置在映射檔案中,使用者需要顯示配置才能開啟。如果無特殊要求,二級緩存的 配置很簡單。如下:

如果我們想修改緩存的一些屬性,可以像下面這樣配置。

根據上面的配置建立出的緩存有以下特點:

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

下面我們來分析一下緩存配置的解析邏輯,如下:

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);

     // 擷取子節點配置
      Properties props = context.getChildrenAsProperties();

     // 建構緩存對象
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
           

上面代碼中,大段代碼用來解析< cache>節點的屬性和子節點, 緩存對象的建構邏輯封裝在 BuilderAssistant 類的 useNewCache 方法中,下面我們來看一下 該方法的邏輯。

// -☆- MapperBuilderAssistant
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
      // 使用建造模式建構緩存執行個體
    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();

   // 添加緩存到 Configuration 對象中
    configuration.addCache(cache);

   // 設定 currentCache 周遊,即目前使用的緩存
    currentCache = cache;
    return cache;
  }

           

上面使用了建造模式建構 Cache 執行個體,Cache 執行個體建構過程略為複雜,我們跟下去看看。

// -☆- CacheBuilder
public Cache build() {
    // 設定預設的緩存類型(PerpetualCache)和緩存裝飾器(LruCache)
    setDefaultImplementations();
    // 通過反射建立緩存
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // 僅對内置緩存 PerpetualCache 應用裝飾器
    if (PerpetualCache.class.equals(cache.getClass())) {
      // 周遊裝飾器集合,應用裝飾器
      for (Class<? extends Cache> decorator : decorators) {
        // 通過反射建立裝飾器執行個體
        cache = newCacheDecoratorInstance(decorator, cache);
        // 設定屬性值到緩存執行個體中
        setCacheProperties(cache);
      }
      // 應用标準的裝飾器,比如 LoggingCache、SynchronizedCache
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      // 應用具有日志功能的緩存裝飾器
      cache = new LoggingCache(cache);
    }
    return cache;
  }
           

上面的建構過程流程較為複雜,這裡總結一下。如下:

  1. 設定預設的緩存類型及裝飾器
  2. 應用裝飾器到 PerpetualCache 對象上
  3. 應用标準裝飾器
  4. 對非 LoggingCache 類型的緩存應用 LoggingCache 裝飾器

    在以上 4 個步驟中,最後一步的邏輯很簡單,無需多說。下面按順序分析前 3 個步驟對 應的邏輯,如下:

private void setDefaultImplementations() {
    if (implementation == null) {
      // 設定預設的緩存實作類
      implementation = PerpetualCache.class;
      if (decorators.isEmpty()) {
        // 添加 LruCache 裝飾器
        decorators.add(LruCache.class);
      }
    }
  }
           

解析< cache-ref>節點

在 MyBatis 中,二級緩存是可以共用的。這需要通過< cache-ref>節點為命名空間配置參 照緩存,比如像下面這樣。

<!-- Mapper1.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper1">
<!-- Mapper1 與 Mapper2 共用一個二級緩存 --> 
      <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>
<!-- Mapper2.xml -->
<mapper namespace="xyz.coolblog.dao.Mapper2"> 
      <cache/>
</mapper>
           

對照上面的配置分析 cache-ref 的解析過程。

private void cacheRefElement(XNode context) {
    if (context != null) {
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      // 建立 CacheRefResolver 執行個體
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
         // 解析參照緩存
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        // 捕捉IncompleteElementException 異常,并将 cacheRefResolver 
        // 存入到 Configuration 的 incompleteCacheRefs 集合中
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }
           

上所示,< cache-ref>節點的解析邏輯封裝在了 CacheRefResolver 的 resolveCacheRef 方 法中,我們一起看一下這個方法的邏輯。

// -☆- MapperBuilderAssistant
public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      // 根據命名空間從全局配置對象(Configuration)中查找相應的緩存執行個體
      Cache cache = configuration.getCache(namespace);
      /* * * * * *
      若未查找到緩存執行個體,此處抛出異常。這裡存在兩種情況導緻未查找到 cache執行個體,分别如下:
      1.使用者在 <cache-ref> 中配置了一個不存在的命名空間, 導緻無法找到 cache 執行個體
      2.使用者所引用的緩存執行個體還未建立
      */
       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);
    }
  }
           

解析< resultMap>節點

resultMap 元素是 MyBatis 中最重要最強大的元素,它可以把大家從 JDBC ResultSets 數 據提取的工作中解放出來。通過 resultMap 和自動映射,可以讓 MyBatis 幫助我們完成 ResultSet → Object 的映射,這将會大大提高了開發效率。

下面開始分析 resultMap 配置的解析過程。

// -☆- XMLMapperBuilder
  private void resultMapElements(List<XNode> list) throws Exception {
    // 周遊 <resultMap> 節點清單
    for (XNode resultMapNode : list) {
      try {
        // 解析 resultMap 節點
        resultMapElement(resultMapNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 擷取 id 和 type 屬性
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));

    // 擷取 extends 和 autoMapping
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 解析 type 屬性對應的類型
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    // 擷取并周遊 <resultMap> 的子節點清單
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        // 解析 constructor 節點,并生成相應的 ResultMapping
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        // 解析 discriminator 節點
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        if ("id".equals(resultChild.getName())) {
          // 添加 ID 到 flags 集合中
          flags.add(ResultFlag.ID);
        }
        // 解析 id 和 property 節點,并生成相應的 ResultMapping
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
    // 根據前面擷取到的資訊建構 ResultMap 對象
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
       /*
        * 如果發生 IncompleteElementException 異常,
        * 這裡将 resultMapResolver 添加到 incompleteResultMaps 集合中 */
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }
           

上面的代碼比較多,看起來有點複雜,這裡總結一下:

  1. 擷取< resultMap>節點的各種屬性
  2. 周遊< resultMap>的子節點,并根據子節點名稱執行相應的解析邏輯
  3. 建構 ResultMap 對象
  4. 若建構過程中發生異常,則将 resultMapResolver 添加到incompleteResultMaps 集合中
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    // 根據節點類型擷取 name 或 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");
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
    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、typeHandler 的類型以及枚舉類型 JdbcType
    Class<?> javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }
           

下面分析 ResultMapping 的建構過程。

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) {
      // 若 javaType 為空,這裡根據 property 的屬性進行解析。關于下面方法中的參數, // 這裡說明一下:
    // - resultType:即 <resultMap type="xxx"/> 中的 type 屬性
// - property:即 <result property="xxx"/> 中的 property 屬性
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    // 解析 TypeHandler
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    // 解析 column = {property1=column1, property2=column2} 的情況,
// 這裡會将 column 拆分成多個 ResultMapping
    List<ResultMapping> composites = parseCompositeColumnName(column);
    // 通過建造模式建構 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<ResultFlag>() : flags)
        .composites(composites)
        .notNullColumns(parseMultipleColumnNames(notNullColumn))
        .columnPrefix(columnPrefix)
        .foreignColumn(foreignColumn)
        .lazy(lazy)
        .build();
  }
// -☆- ResultMapping.Builder
  public ResultMapping build() {
      // 将 flags 和 composites 兩個集合變為不可修改集合
      resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
      resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
      // 從 TypeHandlerRegistry 中擷取相應 TypeHandler
      resolveTypeHandler();
      validate();
      return resultMapping;
    }
           
MyBatis源碼解析(四) --- 解析Mapper節點

解析< sql>節點

< sql>節點用來定義一些可重用的 SQL 語句片段,比如表名,或表的列名等。在映射文 件中,我們可以通過< include>節點引用< sql>節點定義的内容。下面我來示範一下< sql>節點 的使用方式,如下:

<sql id="table"> 
<select id="findOne" resultType="Article">
   SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
</select>
<update id="update" parameterType="Article">
  UPDATE <include refid="table"/> SET title = #{title} WHERE id = #{id}
</update>

</sql>
           

往下分析

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
      // 擷取 id 和 databaseId 屬性
      String databaseId = context.getStringAttribute("databaseId");
      // id = currentNamespace + "." + id
      String id = context.getStringAttribute("id");
      id = builderAssistant.applyCurrentNamespace(id, false);
      // 檢測目前 databaseId 和 requiredDatabaseId 是否一緻
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      // 将 <id, XNode> 鍵值對緩存到 sqlFragments 中
        sqlFragments.put(id, context);
      }
    }
  }
           

這個方法邏輯比較簡單,首先是擷取< sql>節點的 id 和 databaseId 屬性,然後為 id 屬性 值拼接命名空間。最後,通過檢測目前 databaseId 和 requiredDatabaseId 是否一緻,來決定保 存還是忽略目前的節點。下面,我們來看一下 databaseId 的比對邏輯是怎樣的。

解析 SQL 語句節點

前面分析了< cache>、< cache-ref>、< resultMap>以及< sql>節點,從這一節開始,我們來 分析映射檔案中剩餘的幾個節點,分别是< select>、< insert>、< update>以及< delete>等。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 擷取各種屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    // 通過别名解析resultType對應的類型
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    // 解析Statement 類型,預設PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //解析 ResultSetType
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    // 擷取節點的名稱,比如 <select> 節點名稱為 select
    String nodeName = context.getNode().getNodeName();
    // 根據節點名稱解析 SqlCommandType
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 解析 <include> 節點
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析 <selectKey> 節點
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
   // 解析 SQL 語句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

     // 建構 MappedStatement 對象,并将該對象存儲到
     // Configuration 的 mappedStatements 集合中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
           

以 上代碼做的事情如下。

  1. 解析< include>節點
  2. 解析< selectKey>節點
  3. 解析 SQL,擷取 SqlSource
  4. 建構 MappedStatement 執行個體