mybatis-初始化(二)映射文件解析

概述

mapper.xml是我們使用mybatis接觸最多的,需要編寫sql、ResultMap、ParameterMap等。且看如何解析xml,並註冊到Configuration中。

入口

接着上篇mybatis-confg解析的末尾,關於mapper.xml的解析。

先上實例mapper內容:

解析

入口類是 XMLMapperBuilder,無論是單個還是批量都是循環遍歷 XMLMapperBuilder.parse()。

public class XMLMapperBuilder extends BaseBuilder {
  // xml解析工具類
  private final XPathParser parser;
  // Mapper構造小助手(工具類)
  private final MapperBuilderAssistant builderAssistant;
  // sql片段容器,key=sqlId,value=xmlNode
  private final Map<String, XNode> sqlFragments;
  // mapper resource()
  private final String resource;
}

依舊依靠XPathParser解析。

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    // 解析<mapper />
    configurationElement(parser.evalNode("/mapper"));

    // 標記該resource已經加載過,防止重複加載
    configuration.addLoadedResource(resource);

    // 綁定mapper接口 和 xml
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

mapper.xml的根節點是mapper,

來看具體解析過程

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);

    // 解析<cache-ref />
    cacheRefElement(context.evalNode("cache-ref"));

    // 解析<cache />
    cacheElement(context.evalNode("cache"));

    // 解析<parameterMap /> 已廢棄
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));

    // 解析<resultMap />
    resultMapElements(context.evalNodes("/mapper/resultMap"));

    // 解析<sql />
    sqlElement(context.evalNodes("/mapper/sql"));

    // 解析statement
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

這裏依次解析mapper文件中允許出現的標籤,

cache-ref

cache引用,共享cache容器的時候使用。

 private void cacheRefElement(XNode context) {
    if (context != null) {
      // 添加緩存引用到Configuration中
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 根據ref查找該cache是否存在
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }

cache

真的用的很少,簡單掃一眼。

private void cacheElement(XNode context) {
  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);
  }
}

parameterMap

mybatis已經在賤賤廢棄parameterMap了,我們在日常使用的時候也基本不會使用到,這是老版本提供的功能。略過。

private void parameterMapElement(List<XNode> list) {
  for (XNode parameterMapNode : list) {
    String id = parameterMapNode.getStringAttribute("id");
    String type = parameterMapNode.getStringAttribute("type");
    Class<?> parameterClass = resolveClass(type);
    List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
    List<ParameterMapping> parameterMappings = new ArrayList<>();
    for (XNode parameterNode : parameterNodes) {
      String property = parameterNode.getStringAttribute("property");
      String javaType = parameterNode.getStringAttribute("javaType");
      String jdbcType = parameterNode.getStringAttribute("jdbcType");
      String resultMap = parameterNode.getStringAttribute("resultMap");
      String mode = parameterNode.getStringAttribute("mode");
      String typeHandler = parameterNode.getStringAttribute("typeHandler");
      Integer numericScale = parameterNode.getIntAttribute("numericScale");
      ParameterMode modeEnum = resolveParameterMode(mode);
      Class<?> javaTypeClass = resolveClass(javaType);
      JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
      Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
      ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
      parameterMappings.add(parameterMapping);
    }
    builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
  }
}

resultMap

結果集映射,日常使用的較多,也比較複雜,解析過程中主要關注日常使用的簡單標籤。先貼示例的ResultMap內容:

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

這裏還涉及到一些不常見的標籤例如,association、collection、discriminator,可以在官網看下說明,平時也不太推薦使用,我覺得目前隨手可見分庫分表的場景,mapper還是保持簡單,提供最基礎的sql就可以,不要使用什麼高級用法,維護、定位問題都麻煩。

private void resultMapElements(List<XNode> list) throws Exception {
  for (XNode resultMapNode : list) {
    try {
      // 遍歷 解析<resultMap />
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
  return resultMapElement(resultMapNode, Collections.emptyList(), null);
}
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  // 解析type,優先級上到下,四個type的作用是一樣的
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));

  // 通過alias獲取type
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<>();
  resultMappings.addAll(additionalResultMappings);
  List<XNode> resultChildren = resultMapNode.getChildren();

  // 遍歷子節點,解析成ResultMapping
  for (XNode resultChild : resultChildren) {
    // 構造函數 標籤解析
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 鑑別器 標籤解析
    else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    else {
      List<ResultFlag> flags = new ArrayList<>();
      // id標籤解析
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      // 構造resultMapping
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  String id = resultMapNode.getStringAttribute("id",
          resultMapNode.getValueBasedIdentifier());
  String extend = resultMapNode.getStringAttribute("extends");
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");

  // resultMap解析器
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

這裏解析resultMap的步驟:

  1. 解析ResultMapping

    1. 構造器節點解析,構造成ResultMapping。

    2. 鑑別器解析,構造成ResultMapping。

    3. 通用解析,構造成ResultMapping。

  2. 構造ResultMap對象

構造器ResultMapping解析-processConstructorElement

private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
  List<XNode> argChildren = resultChild.getChildren();
  // 遍歷constructor下的子節點
  for (XNode argChild : argChildren) {
    List<ResultFlag> flags = new ArrayList<>();
    flags.add(ResultFlag.CONSTRUCTOR);
    if ("idArg".equals(argChild.getName())) {
      flags.add(ResultFlag.ID);
    }
    
    // 構造ResultMapping並添加進結果中
    resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
  }
}

這裏對構造器ResultMapping都設置了ResultFlag.CONSTRUCTOR,對id標籤額外添加了ResultFlag.ID。

discriminator(幾乎不用,略過)

通用解析

 List<ResultFlag> flags = new ArrayList<>();
  // id標籤解析
  if ("id".equals(resultChild.getName())) {
    flags.add(ResultFlag.ID);
  }
  // 構造resultMapping
  resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}

這裏除了constructor、discriminator這兩種標籤,處理都是相同的。

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
  String 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.emptyList(), resultType));
  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"));
  Class<?> javaTypeClass = resolveClass(javaType);
  Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

解析結果無非就是值對應,並設置到屬性中,比較枯燥。

當有了id、typeClass、ResultMapping之後,ResultMap也就隨即產生。

Sql標籤解析

在一些重複的sql片段上,我們會使用sql標籤進行抽取,以便複用。

private void sqlElement(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      // 直接設置了id 和 xcond的映射
      sqlFragments.put(id, context);
    }
  }
}

Statement解析

這裏遍歷解析mapper下的所有Statement,委託給 XMLStatementBuilder進行解析。

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      //解析異常的,記錄下來
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

這裏Statement的解析也會複雜一些,放到下一篇中單獨講解。

總結

mapper的解析代碼還是蠻簡單的,根據文檔中定義的標籤,按順序解析,解析好的結果會存儲到Configuration中。

不過話說 MapperBuilderAssistant 真是一個神奇的類,它是一個工具類,用於在解析Mapper.xml中提供各位創建相關關鍵類的方法。

例如

public ResultMap addResultMap(
    String id,
    Class<?> type,
    String extend,
    Discriminator discriminator,
    List<ResultMapping> resultMappings,
    Boolean autoMapping) {
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) {

這種方法入參這麼長?不是方法設計的最佳實踐,如果在我們開發過程中出現這樣的方法,那還不被人砍死。

那mybatis這麼做的原因是什麼呢?

首先 比如構造ResultMapping確實需要很多屬性設置,這裏應該是想MapperBuilderAssistant只做純粹的構造過程。

否則可以將入參改爲XNode,在內部進行獲取解析,那麼入參就短爲1個。如果有其他不是通過XNode來創建的場景,那麼就重載方法解決嘍。

這裏考慮到代碼的複用的問題,我覺得在應用開發過程中,可以犧牲一點 依賴獨立 換更多的代碼可讀性。


Thats All

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章