大厂Java面试题:MyBatis映射文件中,A元素通过include引入B元素定义的SQL语句,B元素只能定义在A元素之前吗?

大家好,我是王有志。 今天给大家带来的是一道来自京东的 MyBatis 面试题:MyBatis映射文件中,A元素通过include引入B元素定义的SQL语句,B元素只能定义在A元素之前吗?

答案是否定的,B 元素可以定义在 MyBatis 映射器文件中的任何位置。

首先要明确一点,include 元素只能引入由 sql 元素定义的 SQL 语句片段,而不能引入由其它元素定义的 SQL 语句。而能够通过 include 元素引入 SQL 语句片段的有以下 14 个元素:

  • select 元素
  • insert 元素
  • update 元素
  • delete 元素
  • include 元素
  • sql 元素
  • selectKey 元素
  • trim 元素
  • where 元素
  • set 元素
  • foreach 元素
  • when 元素
  • if 元素
  • otherwise 元素

MyBatis 在读取并解析映射器文件时会优先解析 sql 元素定义的语句,并存储在 Map 容器中,最后再去解析由 select 元素,insert 元素,update 元素和 delete 元素定义的 SQL 语句,这部分源码位于XMLMapperBuilde#rconfigurationElement方法中,源码如下:

csharp 复制代码
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    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);
  }
}

第 13 行中调用了XMLMapperBuilder#sqlElement方法解析 MyBatis 映射器文件中的 sql 元素定义的 sql 语句片段,源码如下:

typescript 复制代码
private void sqlElement(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}

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)) {
      sqlFragments.put(id, context);
    }
  }
}

可以看到,最终解析完成的由 sql 元素定义的 SQL 语句片段会存储到成员变量 sqlFragments 中。

接着我们来看XMLMapperBuilder#buildStatementFromContext方法,该方法负责解析由 select 元素,insert 元素,update 元素和 delete 元素定义的 SQL 语句,方法源码如下:

scss 复制代码
private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

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

我们一路追踪到调用XMLStatementBuilder#parseStatementNode方法,该方法很长,我们只关注解析 include 元素的部分:

csharp 复制代码
public void parseStatementNode() {
  // 省略
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());
  // 省略
}

最终调用到XMLIncludeTransformer#applyIncludes方法,这里我跳过了一个重载方法,我们直接看处理的核心部分:

scss 复制代码
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  if ("include".equals(source.getNodeName())) {
    // 通过refId获取由sql元素定义的SQL语句片段
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
    Properties toIncludeContext = getVariablesContext(source, variablesContext);

    // 使用sql元素定义的SQL语句片段进入递归调用,用于处理SQL语句片段中由include引入的内容
    applyIncludes(toInclude, toIncludeContext, true);
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    // 使用toInclude节点替换source节点的父节点的子节点(即source节点),这里用于重新构建节点间的关系
    // 即使用将原始节点中通过include元素引入的内容替换为sql元素中定义的SQL语句片段
    source.getParentNode().replaceChild(toInclude, source);
    while (toInclude.hasChildNodes()) {
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    toInclude.getParentNode().removeChild(toInclude);
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {
    if (included && !variablesContext.isEmpty()) {
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {
        Node attr = attributes.item(i);
        attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
      }
    }
    NodeList children = source.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      applyIncludes(children.item(i), variablesContext, included);
    }
  } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE) && !variablesContext.isEmpty()) {
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

private Node findSqlFragment(String refid, Properties variables) {
  refid = PropertyParser.parse(refid, variables);
  refid = builderAssistant.applyCurrentNamespace(refid, true);
  try {
    // 通过refid获取由sql元素定义的SQL语句片段
    XNode nodeToInclude = configuration.getSqlFragments().get(refid);
    return nodeToInclude.getNode().cloneNode(true);
  } catch (IllegalArgumentException e) {
    throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
  }
}

XMLIncludeTransformer#applyIncludes方法是个递归调用,如果直接通过源码和我标注的注释来构建调用链路的话可能会比较困难,建议通过断点调试的方式来分析这段代码的功能,或者通过一些 AI 助手去解释这段代码后再去调试,会更加清晰。


好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠 王有志,我们下次再见!

相关推荐
全栈派森22 分钟前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse27 分钟前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭2 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架2 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱2 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜2 小时前
Flask框架搭建
后端·python·flask
进击的雷神2 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala
进击的雷神2 小时前
Perl测试起步:从零到精通的完整指南
开发语言·后端·scala
豌豆花下猫3 小时前
Python 潮流周刊#102:微软裁员 Faster CPython 团队(摘要)
后端·python·ai
秋野酱3 小时前
基于javaweb的SpringBoot驾校预约学习系统设计与实现(源码+文档+部署讲解)
spring boot·后端·学习