MyBatis - mapper.xml的解析机制

最后一个节点的解析是 mapper ,也就是解析 MyBatis 全局配置文件中,引入的 mapper.xml 的那些路径。而这里面的解析,都是使用一个 XMLMapperBuilder 的 API 完成的。:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 包扫描Mapper接口
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // 处理resource加载的mapper.xml
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    // 处理url加载的mapper.xml
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    // 注册单个Mapper接口
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

那么我们接下来研究的重点,就是深入 XMLMapperBuilder 中,看看它如何解析 mapper.xml 的。

Debug 的载体,我们依然选用一开始复制进来的 MyBatisApplication6 ,Debug 运行后进入 mapperElement 方法,断点停住。

1. XMLMapperBuilder

先大体看一下 XMLMapperBuilder 本身吧,它的内部构造还是比较值得研究的。

1.1 继承关系和内部成员

翻开源码,从继承关系上赫然发现 XMLMapperBuilder ,也是继承自 BaseBuilder 的:

public class XMLMapperBuilder extends BaseBuilder {

  private final XPathParser parser;
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  private final String resource;

是不是再一次体会到之前第 7 章说的,BaseBuilder 是一个基础的构造器啊。

然后关注一下成员:

  • XPathParser parser :很熟悉了,第 7 章就知道它是解析 xml 文件的解析器,此处也用来解析 mapper.xml
  • MapperBuilderAssistant builderAssistant :构造 Mapper 的建造器助手(至于为什么是助手,简单地说,它的内部使用了一些 Builder ,帮我们构造 ResultMap 、MappedStatement 等,不需要我们自己操纵,所以被称之为 “助手” )
  • Map<String, XNode> sqlFragments :封装了可重用的 SQL 片段(就是上一章提到的 <sql> 片段)
  • String resource :mapper.xml 的文件路径

一眼看下来,也没什么特别好强调的,下面碰到什么再专门拿出来说吧。

1.2 构造方法定义

上面的源码中都会先调用 XMLMapperBuilder 的好几个参数的构造方法,而构造方法向下走之后,都是一组赋值操作,也没什么意思。

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
            configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
}

只需要注意一个小细节即可:MapperBuilderAssistant 在此处创建。这个 MapperBuilderAssistant 具体都干了什么,后面我们马上就可以看到了。

1.3 核心parse方法

构造完成后就可以调用 parse 方法了(此时 mapper.xml 已经被 IO 读取封装为 InputStream ),而这个方法的信息量有点大,我们一行一行解析。先留个大体的注释在源码中:

public void parse() {
    // 如果当前xml资源还没有被加载过
    if (!configuration.isResourceLoaded(resource)) {
        // 2. 解析mapper元素
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        // 3. 解析和绑定命名空间
        bindMapperForNamespace();
    }

    // 4. 解析resultMap
    parsePendingResultMaps();
    // 5. 解析cache-ref
    parsePendingCacheRefs();
    // 6. 解析声明的statement
    parsePendingStatements();
}

下面我们就源码中标注了序号的关键代码,逐行解析。不过具体特别深入的我们不做探究,后面小册有专门解析生命周期和执行流程的章节,到那时候我们再展开仔细研究 MyBatis 内部的细节。

2. configurationElement

configurationElement(parser.evalNode("/mapper")); 这句代码只从最后的参数,就知道是解析 mapper.xml 的最顶层 <mapper> 标签了。这部分的解析,会把所有的标签都扫一遍,具体我们可以先看一眼源码和注释:

private void configurationElement(XNode context) {
    try {
        // 提取mapper.xml对应的命名空间
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // 解析cache、cache-ref
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        // 解析提取parameterMap(官方文档称已废弃,不看了)
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析提取resultMap
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析封装SQL片段
        sqlElement(context.evalNodes("/mapper/sql"));
        // 构造Statement
        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);
    }
}

然后我们逐行解释这些标签的解析,底层都干了什么。

2.1 提取命名空间

    // 提取mapper.xml对应的命名空间
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);

初学 MyBatis 的时候,我们就知道,每个 mapper.xml 都需要声明 namespace ,哪怕是我们瞎写那种 abcdefg 的都行,但不能没有,源码中这里就体现了非空检查。命名空间非空的设计,一方面是考虑到二级缓存(一个 namespace 对应一个二级缓存),另一方面也是考虑到可能不同的 mapper.xml 中存在同名的 statement (比方说 department 和 user 都有 findAll ,这个时候通过 namespace 就可以很好地区分开这两个 statement 了)。

2.2 解析cache、cache-ref

    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));

有关 <cache> 、<cache-ref> 标签,小册之前在第 8 章就说过了,咱这里只是简单提一下,二级缓存后面专门有一章讲解。

这两步的核心动作,是解析看一下 mapper.xml 中有没有引用其他 namespace 的二级缓存,以及看一下本 namespace 下有没有开启二级缓存,如果有的话,自己配置一下。至于底层的配置,我们放到后面二级缓存中再探讨。

2.3 解析提取resultMap【复杂】

    // 解析提取resultMap
    resultMapElements(context.evalNodes("/mapper/resultMap"));

前面第 8 章我们就说到,resultMap 结果集映射配置是 MyBatis 最强大的特性之一,自然它的处理逻辑会相当复杂,小伙伴们先有一个思想准备,深吸一口气,我们杀进去看看里面都搞了什么鬼。

private void resultMapElements(List<XNode> list) {
    for (XNode resultMapNode : list) {
        try {
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
            // ignore, it will be retried
        }
    }
}

一个 mapper.xml 文件可能不止一个 <resultMap> 标签,这里肯定有一个 for 循环啦。

注意这里的 try-catch 结构是放在 for 循环体里的,这么做是为了防止某一个 resultMap 解析失败时,导致连带着 mapper.xml 中其他的 resultMap 也没法解析。这样设计后,即便某一个 resultMap 解析挂掉了,也可以继续解析剩余的 resultMap 。

进入单个 resultMap 的解析方法,这里面的逻辑看上去挺多,但实际上条理还是很清晰的,我们可以先通读一遍源码,配合着我标注的注释理解一下:

private ResultMap resultMapElement(XNode resultMapNode) {
    return resultMapElement(resultMapNode, Collections.emptyList(), null);
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 解析resultMap映射的目标结果集实体类型
    String type = resultMapNode.getStringAttribute("type",
                         resultMapNode.getStringAttribute("ofType", 
                         resultMapNode.getStringAttribute("resultType", 
                         resultMapNode.getStringAttribute("javaType"))));
    // 加载目标结果集实体类型
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 解析resultMap的子标签,并封装为resultMapping
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    
    // 获取resultMap的id、继承的resultMap id、autoMapping
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 利用ResultMapResolver处理resultMap
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, 
            typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        // 解析失败,说明resultMap标签的信息不完整,记录在全局Configuration中,并抛出异常
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

走完一遍源码,是不是感觉没有很混乱?思路还是蛮清晰的?其实这也是体现了 MyBatis 的源码中清晰的逻辑思路。下面我们分段来解释这段源码中比较复杂的部分。

2.3.1 解析结果集目标类型

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
                         resultMapNode.getStringAttribute("ofType", 
                         resultMapNode.getStringAttribute("resultType", 
                         resultMapNode.getStringAttribute("javaType"))));
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    // ......

这一段的目的就是解析目标结果集的实体类类型,上面提到的 4 个属性都可以写,而且必定得有一个写,如果都不写,在解析 xml 时就会报 DTD 异常(需要属性 "type" , 并且必须为元素类型 "resultMap" 指定该属性)。优先级依次是 type > ofType > resultType > javaType 。

2.3.2 解析结果集映射配置

    // ......
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 解析resultMap的子标签,并封装为resultMapping
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    // ......

这一段又很复杂,这里主要干的活是解析 <resultMap> 的子标签们,由于只有 3 种标签可以写(普通映射、构造器映射、鉴别器映射),所以这里的 if-else 结构也看上去比较简单,当然也仅限于看上去。内部解析这几个子标签的内容又比较复杂了,我们先从最简单的 else 中看起。

2.3.2.1 解析普通映射标签

普通映射标签有 id 、result 、association 、collection 四个标签,也都是我们在学习 MyBatis 基础的时候就用到的标签了。解析的核心方法是 buildResultMappingFromContext ,我们进去看一下:(辣眼警告)

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        property = context.getStringAttribute("name");
    } else {
        property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap", () ->
            processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", 
                             configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    // 结果集类型、typeHandler类型的解析
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, 
                   javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, 
                   notNullColumn, columnPrefix, typeHandlerClass, 
                   flags, resultSet, foreignColumn, lazy);
}

好家伙,合着这一段都是取属性呗,那就没意思了呀(就是有点辣眼)。。。

这个方法我们要知道的,就是将一行 <result property="" column="" /> 标签解析封装为一个 ResultMapping 即可。具体的细节,我们到后面的生命周期章节中再深入剖析。

2.3.2.2 处理constructor

前面第 8 章我们已经接触了 <constructor> 标签的使用,也知道它的内部其实还是封装类似于 <id> 、<result> 等标签,所以它的处理逻辑基本上是大同小异:

private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
        List<ResultFlag> flags = new ArrayList<>();
        flags.add(ResultFlag.CONSTRUCTOR);
        if ("idArg".equals(argChild.getName())) {
            flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
}

得了,这不跟上面一个样了吗?最终还是调用的 buildResultMappingFromContext 方法,把那一行一行的结果集映射都封装为 ResultMapping 完事。

不过这里要额外注意一个细节,上面的每一行结果集映射中,都会对应一个 List<ResultFlag> flags 的家伙,而且在解析 <constructor> 标签的时候,它会先给 flags 集合中添加一个 ResultFlag.CONSTRUCTOR 的元素,这个元素会在 buildResultMappingFromContext 方法中起作用:

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        // <constructor>标签用name
        property = context.getStringAttribute("name");
    } else {
        // 普通的标签用property
        property = context.getStringAttribute("property");
    }

可以发现,<constructor> 标签中取属性名要用 name 而不是 property ,这个小细节我们可以在 mapper.xml 中发现:

MyBatis - mapper.xml的解析机制

或许我们没有感知到,一是因为一般情况下结果集映射的实体类都只有缺省的无参构造器,用不到 <constructor> 属性;二是写的时候也没有特别的去找,看到 name 或许也会理所当然的觉得它就是(常码代码的各位都有一种所谓的“感觉”,不怎么过大脑思考就顺手写出来了,有同感的记得评论区扣**【俺也一样】**)。

2.3.2.3 处理discriminator

鉴别器的解析是最特别的一个了,它最终构建的类型都不一样了,咱先扫一下源码:

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 解析<discriminator>的<case>子标签,并封装到Map中
    Map<String, String> discriminatorMap = new HashMap<>();
    for (XNode caseChild : context.getChildren()) {
        String value = caseChild.getStringAttribute("value");
        String resultMap = caseChild.getStringAttribute("resultMap", 
                processNestedResultMappings(caseChild, resultMappings, resultType));
        discriminatorMap.put(value, resultMap);
    }
    // 注意构造的是Discriminator而不是ResultMapping
    return builderAssistant.buildDiscriminator(resultType, column, 
             javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

粗略的扫一遍源码,我们至少可以得知两件事情:1)最终封装的是一个 Discriminator 对象,同样由 MapperBuilderAssistant 构建;2)<discriminator> 的子标签 <case> 最终会封装到一个 Map 中。至于封装的 Map 里到底是啥,我们也是放到后面生命周期的章节中讲解。

2.3.3 封装构建ResultMap

<resultMap> 标签解析的最后一部分,它会用一个 ResultMapResolver 来处理,并最终构造出 ResultMap 对象。

    // ......
    // 获取resultMap的id、继承的resultMap id、autoMapping
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 利用ResultMapResolver处理resultMap
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, 
            typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        // 解析失败,说明resultMap标签的信息不完整,记录在全局Configuration中,并抛出异常
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

这个 ResultMapResolver ,说起来有点搞笑,它的 resolve 方法,就是调了 MapperBuilderAssistant 的方法:

public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

那问题就来了,它为什么要搞这么一出呢?闲得慌吗?哎,人家这么设计肯定是有原因的,这里是一个伏笔,下面第 4 小节会有呼应。

至于 MapperBuilderAssistant 底层都干了什么,现在深入进去难免各位会头晕,所以咱都是跟上面一样,统一放在后面的生命周期章节讲解,这部分各位只需要知道:最终干活的都是 MapperBuilderAssistant 就得了。

2.4 提取SQL片段

接下来是提取各个 mapper.xml 中的 SQL 片段了,这里面的大规则我们都很清楚了:如果有显式声明 databaseId ,那只有符合当前全局 databaseId 的 SQL 片段会提取;如果没有声明 databaseId ,则会全部提取

所以下面的源码中,我们会发现,它解析了两遍 SQL 片段,而且在每一次循环解析中,都会判断一次 SQL 片段是否匹配当前 databaseId ,匹配的话就会放到一个 sqlFragments 的 Map 中:(关键代码已标有注释)

private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        // 先全部过一遍,提取出匹配SQL片段的statement
        sqlElement(list, configuration.getDatabaseId());
    }
    // 再提取通用的SQL片段
    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);
        // 鉴别当前SQL片段是否匹配
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            sqlFragments.put(id, context);
        }
    }
}

可以发现处理逻辑是很简单的是吧!这里面的匹配 SQL 片段的逻辑还蛮有意思的,我们可以研究一下:

(小册只把逻辑贴出来,具体的几种情况小伙伴们可以自行推断一下,完全符合 MyBatis 的设计思路)

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    // 显式配置了需要databaseId,那就直接匹配
    if (requiredDatabaseId != null) {
        return requiredDatabaseId.equals(databaseId);
    }
    // 不需要databaseId,但这个SQL片段有声明,则一律不收
    if (databaseId != null) {
        return false;
    }
    // 还没有存过这条SQL片段,则直接收下
    if (!this.sqlFragments.containsKey(id)) {
        return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    // 已经存过了?拿出来看看是不是有databaseId,如果有,那就说明存在同id但没有配置databaseId的,不管了
    // (存在同id的情况下,有databaseId的优先级比没有的高)
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
}

2.5 解析statement【复杂】

最后一部分又是很复杂的了,它会解析 mapper.xml 中声明的 <select> 、<insert> 、<update> 、<delete> 标签,并最终封装为一个一个的 MappedStatement 。有关 databaseId 的处理逻辑,还是跟 SQL 片段一样,小册不多重复,我们要关注的还是如何处理和解析这些 statement 的标签们:

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 {
            // 【复杂、困难】借助XMLStatementBuilder解析一个一个的statement标签
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            // statement解析失败,只会记录到Configuration中,但不会抛出异常
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

通读一遍源码后,似乎脑子里只有一件事:它又把活交给别人干了。。。没错,解析 statement 的工作交给 XMLStatementBuilder 了,注意又是一个 Builder !现在我们正在解析使用的 XmlMapperBuilder 也是一个 Builder !所以可想而知里面的源码估计又是这么一大堆!好吧,我们还是放到后面生命周期的章节再展开吧,现在就展开,怕是小伙伴要疯掉了。。。(狗头保命)

OK ,这么一遍走下来,基本上所有的标签元素也就都扫完了,我们接下来回到 XmlMapperBuilder 的 parse 方法中,继续往下走。

3. bindMapperForNamespace

接下来要执行的 bindMapperForNamespace 方法,本质上是为了 Mapper 接口动态代理而设计的,我们都知道,利用 Mapper 动态代理的特性,可以使得我们可以直接取 Mapper 接口,而不用操纵 SqlSession 的 API ,写那一堆复杂的 statementId ,而且也相对更容易维护代码了。

这个 bindMapperForNamespace 的方法,就是为这个特性做的支撑,我们来看看它的底层实现:

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 尝试加载namespace对应的类
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
        }
        // 加载到类了,并且之前没存过这个Mapper接口,那就存起来
        if (boundType != null && !configuration.hasMapper(boundType)) {
            // Spring may not know the real resource name so we set a flag
            // to prevent loading again this resource from the mapper interface
            // look at MapperAnnotationBuilder#loadXmlResource
            // Spring可能不知道真实的资源名称,因此设置了一个标志来防止再次从Mapper接口加载此资源
            configuration.addLoadedResource("namespace:" + namespace);
            configuration.addMapper(boundType);
        }
    }
}

嚯,这操作也忒简单了吧!直接把 namespace 拿来,用类加载去加载对应的类,如果加载到了,就把它存起来,完事;如果没加载到,那就当无事发生。是的,这本身的逻辑可以说是相当简单了,不过这里面有个细节,也就是源码中的几行单行注释:

Spring may not know the real resource name so we set a flag to prevent loading again this resource from the mapper interface.

Spring 可能不知道真实的资源名称,因此设置了一个标志来防止再次从 Mapper 接口加载此资源。

这个操作是图个啥呢?小册来解释一下原因。

在学习 MyBatis 基础的时候,讲到 Mapper 接口动态代理时,应该各位都记得有一个约定吧:在 MyBatis 全局配置文件中配置 mapper 时,如果使用包扫描的方式,扫描 Mapper 接口时,需要 Mapper 接口与对应的 mapper.xml 放在同一个目录下,且 Mapper 接口的名称要与 mapper.xml 的名称一致。这个约定的底层原理,是 Mapper 接口包扫描时,会自动寻找同目录下的同名称的 mapper.xml 文件并加载解析(核心代码可参照 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#loadXmlResource )。但这个时候就有可能出现意外情况:如果我们既配置了 mapper.xml 的资源路径,又配置了 Mapper 接口包扫描,那岂不是要加载两边?这很明显不是很合理吧!所以 MyBatis 帮我们考虑了这一层,就在这里面加了一个额外的标识:每当解析到一个存在的 Mapper 接口时,会标记这个接口对应的 mapper.xml 文件已加载,这样即便又进行包扫描时读到了这个 Mapper 接口,当它要去加载 mapper.xml 时检查到已经加载过了,就不会再重复加载了。

大段的文字描述可能不是好理解,我们还是用最容易接受的表情包环节来辅助小伙伴们理解吧!

比方说现在我们有 3 个 mapper.xml 文件,当 MyBatis 初始化的时候,全局的 XMLMapperBuilder 会把它们都召集起来,让它们都加载起来,并且加载的时候,派一个专门的人把这些加载过的 mapper.xml 都记下来。

MyBatis - mapper.xml的解析机制

加载完 mapper.xml 之后,下一步 MyBatis 会换另外的人去喊 Mapper 接口,告诉他们也准备加载。

MyBatis - mapper.xml的解析机制

Mapper 接口要加载的时候,觉得自己很重要呀,于是就要拿自己所在的路径,去找对应同名的 mapper.xml 文件。巧了,可能它还真找到同名的 mapper.xml 了,那太开心了呀,它就想去加载,这个时候上面那个记名的人突然出现在他的面前,告诉他:人家都加载过了,你走吧!Mapper 接口虽然很郁闷,但人家确实加载了,它也说不出啥来,就只好悻悻而归了。

MyBatis - mapper.xml的解析机制

由此的一番控制,MyBatis 就可以控制好只加载一次 mapper.xml 文件了,同时也能把可以绑定 Mapper 接口的也都整理好。

4. 重新处理不完整的元素

parse 方法的最后 3 个步骤其实都是干的同一类事情,那就是重新处理一下前面解析过程中保存的残缺不全的元素们:

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();

我们以 resultMap 为例看一下里面的实现:

private void parsePendingResultMaps() {
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
        Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
        while (iter.hasNext()) {
            try {
                // 逐个提取,重新解析
                iter.next().resolve();
                iter.remove();
            } catch (IncompleteElementException e) {
                // ResultMap is still missing a resource...
            }
        }
    }
}

public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

注意这里提取出来的是一组 ResultMapResolver ,刚好呼应上面 2.3.3 节提到的那个看似多余的封装操作!可见这个 ResultMapResolver 并不是多余的做这么一步,通过 ResultMapResolver 这么一个中间层的传递,可以把一个 resultMap 中涉及到的所有定义信息都存起来,这样在重新处理的时候,可以直接把那些解析好的 resultMap 的信息都找回来,并直接让 MapperBuilderAssistant 再试一次

走完一遍迭代后,能正常解析的会从 incompleteResultMaps 中移除,剩余的还在里面继续呆着。当然这个时候再解析不了的 resultMap 也好,statement 也好,MyBatis 还没有彻底放弃它们。在 MyBatis 与 SpringFramework 的整合中,IOC 容器刷新完成后,会最后一次解析这些残缺不全的 resultMap 等等,这部分内容我们放到 MyBatis 整合 SpringFramework 的章节中再展开。


OK ,走到这里,XMLMapperBuilder 的整个处理逻辑也就全部执行完毕了,整个 mapperElement 的方法处理也就完成了,这也就意味着 SqlSessionFactory 已经成功创建出来了。

上一篇:Mybatlis SQL 注入与防范


下一篇:(11) <resultMap>标签