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

相关推荐
jokerest1234 小时前
web——sqliabs靶场——第十三关——报错注入+布尔盲注
mybatis
武子康5 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
qq_17448285755 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍6 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦6 小时前
Scala的Array(2)
开发语言·后端·scala
2401_882727576 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
心仪悦悦7 小时前
Scala中的集合复习(1)
开发语言·后端·scala
代码小鑫7 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖7 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
激流丶7 小时前
【Kafka 实战】Kafka 如何保证消息的顺序性?
java·后端·kafka