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

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