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

相关推荐
AskHarries2 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
isolusion3 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder4 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚4 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
Wyang_XXX4 小时前
CSS 选择器和优先级权重计算这么简单,你还没掌握?一篇文章让你轻松通关面试!(下)
面试
机器之心5 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴6 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven