大厂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 技术的金融摸鱼侠 王有志,我们下次再见!

相关推荐
也无晴也无风雨31 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
不二人生5 小时前
SQL面试题——连续出现次数
hive·sql·面试
颜淡慕潇5 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
清酒伴风(面试准备中......)6 小时前
Java集合 List——针对实习面试
java·面试·list·实习
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
代码小鑫7 小时前
A025-基于SpringBoot的售楼管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计