11 鸟瞰 MyBatis 初始化,把握 MyBatis 启动流程脉络(下) 在上一讲,我们深入分析了MyBatis 初始化过程中对 mybatis-config.xml 全局配置文件的解析,详细介绍了其中每个标签的解析流程以及涉及的经典设计模式——构造者模式。这一讲我们就紧接着上一讲的内容,继续介绍 MyBatis 初始化流程,重点介绍Mapper.xml 配置文件的解析以及 SQL 语句的处理逻辑。

Mapper.xml 映射文件解析全流程

在上一讲分析 mybatis-config.xml 配置文件解析流程的时候我们看到,在 mybatis-config.xml 配置文件中可以定义多个

标签指定 Mapper 配置文件的地址,**MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析**。 与 XMLConfigBuilder 类似,XMLMapperBuilder也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 Mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下: * 执行 configurationElement() 方法解析整个Mapper.xml 映射文件的内容; * 获取当前 Mapper.xml 映射文件指定的 Mapper 接口,并进行注册; * 处理 configurationElement() 方法中解析失败的 标签; * 处理 configurationElement() 方法中解析失败的 标签; * 处理 configurationElement() 方法中解析失败的SQL 语句标签。 可以清晰地看到,**configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方**,其中定义了处理 Mapper.xml 映射文件的核心流程: * 获取 标签中的 namespace 属性,同时会进行多种边界检查; * 解析 标签; * 解析 标签; * 解析 标签; * 解析 标签; * 解析 标签中的select 语句定义,它会将当前列的值作为参数传入这个 select 语句。由于当前结果集可能查询出多行数据,那么可能就会导致 select 属性指定的 SQL 语句会执行多次,也就是著名的 N+1 问题。 * columnPrefix(String 类型):当前标签的 columnPrefix 属性值,记录了表中列名的公共前缀。 * resultSet(String 类型):当前标签的 resultSet 属性值。 * lazy(boolean 类型):当前标签的fetchType 属性,表示是否延迟加载当前标签对应的列。 介绍完 ResultMapping 对象(即 标签下各个子标签的解析结果)之后,我们再来看 标签如何被解析。整个 标签最终会被解析成 ResultMap 对象,它与 ResultMapping 之间的映射关系如下图所示: ![图片1.png](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e5%89%96%e6%9e%90%20MyBatis%20%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86-%e5%ae%8c/assets/CioPOWA7kqSASvnUAAPk5cQ7q3c025.png) ResultMap 结构图 通过上图我们可以看出,ResultMap 中有四个集合与 ResultMapping 紧密相连。 * resultMappings 集合,维护了整个 标签解析之后得到的全部映射关系,也就是全部 ResultMapping 对象。 * idResultMappings 集合,维护了与唯一标识相关的映射,例如, 标签、 标签下的 子标签解析得到的 ResultMapping 对象。如果没有定义 等唯一性标签,则由 resultMappings 集合中全部映射关系来确定一条记录的唯一性,即 idResultMappings 集合与 resulMappings 集合相同。 * constructorResultMappings 集合,维护了 标签下全部子标签定义的映射关系。 * propertyResultMappings 集合,维护了不带 Constructor 标志的映射关系。 除了上述四个 ResultMapping 集合,ResultMap 中还维护了下列核心字段。 * id(String 类型):当前 标签的 id 属性值。 * type(Class 类型):当前 的 type 属性值。 * mappedColumns(Set 类型):维护了所有映射关系中涉及的 column 属性值,也就是所有的列名(或别名)。 * hasNestedResultMaps(boolean 类型):当前 标签是否嵌套了其他 标签,即这个映射关系中指定了 resultMap属性,且未指定 resultSet 属性。 * hasNestedQueries(boolean 类型):当前 标签是否含有嵌套查询。也就是说,这个映射关系中是否指定了 select 属性。 * autoMapping(Boolean 类型):当前 ResultMap 是否开启自动映射的功能。 * discriminator(Discriminator 类型):对应 标签。 接下来我们开始深入分析 标签解析的流程。XMLMapperBuilder的resultMapElements() 方法负责解析 Mapper 配置文件中的全部 标签,其中会通过 resultMapElement() 方法解析单个 标签。 下面是 resultMapElement() 方法解析 标签的核心流程。 * 获取 标签的type 属性值,这个值表示结果集将被映射成 type 指定类型的对象。如果没有指定 type 属性的话,会找其他属性值,优先级依次是:type、ofType、resultType、javaType。在这一步中会确定映射得到的对象类型,这里支持别名转换。 * 解析 标签下的各个子标签,每个子标签都会生成一个ResultMapping 对象,这个 ResultMapping 对象会被添加到resultMappings 集合(List 类型)中暂存。这里会涉及 等子标签的解析。 * 获取 标签的id 属性,默认值会拼装所有父标签的id、value 或 property 属性值。 * 获取 标签的extends、autoMapping 等属性。 * 创建 ResultMapResolver 对象,ResultMapResolver 会根据上面解析到的ResultMappings 集合以及 标签的属性构造 ResultMap 对象,并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中。 ### (1)解析 标签 在 resultMapElement() 方法中获取到 id 属性和 type 属性值之后,会调用 buildResultMappingFromContext() 方法解析上述标签得到 ResultMapping 对象,其核心逻辑如下: * 获取当前标签的property的属性值作为目标属性名称(如果 标签使用的是 name 属性); * 获取 column、javaType、typeHandler、jdbcType、select 等一系列属性,与获取 property 属性的方式类似; * 根据上面解析到的信息,调用 MapperBuilderAssistant.buildResultMapping() 方法创建 ResultMapping 对象。 正如 resultMapElement() 方法核心步骤描述的那样,经过解析得到 ResultMapping 对象集合之后,会记录到resultMappings 这个临时集合中,然后由 ResultMapResolver 调用 MapperBuilderAssistant.addResultMap() 方法创建 ResultMap 对象,将resultMappings 集合中的全部 ResultMapping 对象添加到其中,然后将ResultMap 对象记录到 Configuration.resultMaps 集合中。 下面是 MapperBuilderAssistant.addResultMap() 的具体实现: public ResultMap addResultMap( String id, Class type, String extend, Discriminator discriminator, List resultMappings, Boolean autoMapping) { // ResultMap的完整id是"namespace.id"的格式 id = applyCurrentNamespace(id, false); // 获取被继承的ResultMap的完整id,也就是父ResultMap对象的完整id extend = applyCurrentNamespace(extend, true); if (extend != null) { // 针对extend属性的处理 // 检测Configuration.resultMaps集合中是否存在被继承的ResultMap对象 if (!configuration.hasResultMap(extend)) { throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'"); } // 获取需要被继承的ResultMap对象,也就是父ResultMap对象 ResultMap resultMap = configuration.getResultMap(extend); // 获取父ResultMap对象中记录的ResultMapping集合 List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings()); // 删除需要覆盖的ResultMapping集合 extendedResultMappings.removeAll(resultMappings); // 如果当前标签中定义了标签,则不需要使用父ResultMap中记录 // 的相应标签,这里会将其对应的ResultMapping对象删除 boolean declaresConstructor = false; for (ResultMapping resultMapping : resultMappings) { if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { declaresConstructor = true; break; } } if (declaresConstructor) { extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)); } // 添加需要被继承下来的ResultMapping对象记录到resultMappings集合中 resultMappings.addAll(extendedResultMappings); } // 创建ResultMap对象,并添加到Configuration.resultMaps集合中保存 ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) .discriminator(discriminator) .build(); configuration.addResultMap(resultMap); return resultMap; } 至于 标签的流程,是由XMLMapperBuilder 中的processConstructorElement() 方法实现,其中会先获取 标签的全部子标签,然后为每个标签添加 CONSTRUCTOR 标志(为每个 标签添加额外的ID标志),最后通过 buildResultMappingFromContext()方法创建 ResultMapping对象并记录到 resultMappings 集合中暂存,这些 ResultMapping 对象最终也会添加到前面介绍的ResultMap 对象。 ### (2)解析 标签 接下来,我们来介绍解析 标签的核心流程,两者解析的过程基本一致。前面介绍的 buildResultMappingFromContext() 方法不仅完成了 等标签的解析,还完成了 标签的解析,其中相关的代码片段如下: private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) { ... // 标签中其他属性的解析与标签类似,这里不再展开 // 如果标签没有指定resultMap属性,那么就是匿名嵌套映射,需要通过 // processNestedResultMappings()方法解析该匿名的嵌套映射 String nestedResultMap = context.getStringAttribute("resultMap", () -> processNestedResultMappings(context, Collections.emptyList(), resultType)); ... // 标签中其他属性的解析与标签类似,这里不再展开 // 根据上面解析到的属性值,创建ResultMapping对象 return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); } 这里的 processNestedResultMappings() 方法会递归执行resultMapElement() 方法解析 标签和 标签指定的匿名嵌套映射,得到一个完整的ResultMap 对象,并添加到Configuration.resultMaps集合中。 ### (3)解析 标签 最后一个要介绍的是 标签的解析过程,我们将 标签与 标签配合使用,根据结果集中某列的值改变映射行为。从 resultMapElement() 方法的逻辑我们可以看出, 标签是由 processDiscriminatorElement() 方法专门进行解析的,具体实现如下: private Discriminator processDiscriminatorElement(XNode context, Class resultType, List resultMappings) { // 从标签中解析column、javaType、jdbcType、typeHandler四个属性的逻辑非常简单,这里将这部分代码省略 Map<String, String> discriminatorMap = new HashMap<>(); // 解析标签的子标签 for (XNode caseChild : context.getChildren()) { String value = caseChild.getStringAttribute("value"); // 通过前面介绍的processNestedResultMappings()方法,解析标签, // 创建相应的嵌套ResultMap对象 String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType)); // 记录该列值与对应选择的ResultMap的Id discriminatorMap.put(value, resultMap); } // 创建Discriminator对象 return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); } ### SQL 语句解析全流程 在 Mapper.xml 映射文件中,除了上面介绍的标签之外,还有一类比较重要的标签,那就是