Mybatis源码之美:3.1.负责解析mapper文件的XmlMapperBuilder

2,639 阅读5分钟

负责解析mapper文件的XmlMapperBuilder

XmlMapperBuilderBaseBuilder的一个实现,他的作用主要是通过解析Mybatismapper文件来完成Mapper对象的创建和初始化工作。

mapper文件是指用于配置Mybatis映射器Mapper对象的xml文件。

比如: Mapper.xml:

<mapper namespace="org.apache.learning.typehandler.Mapper">
    ...
</mapper>

Mapper.java:

package org.apache.learning.typehandler;

public interface Mapper {
    ...
}

在开始了解XmlMapperBuilder的具体解析过程之前,我们需要先熟悉一下mapper文件的定义。

Mybatismapper文件的根元素为mapper,他有一个必填的namespace属性,正常来说,namespace属性的取值是当前mapper文件对应的Mapper接口定义的全限定名称。

namespace属性定义的命名空间的值在整个Mybatis中应该是唯一的,通过namespacemybatis对数据在逻辑上进行了隔离,确保不同Mapper之间的数据不会互相污染的同时,还提供了跨namespace引用数据配置的能力。

 XMLMapperBuilder mapperParser = new XMLMapperBuilder(
        inputStream/*XML文件输入流*/
        , configuration/*Mybatis用户全局配置*/
        , url/*资源配置文件的地址*/
        , configuration.getSqlFragments()/*已有的XML代码块*/
);

对于XmlMapperBuilder的构造器参数,我们唯一不熟悉的应该是configuration.getSqlFragments(),Configuration#sqlFragments是一个用来保存代码块映射的集合,具体作用在接下来的解析过程中我们会讲到。

XmlMapperBuilder在构造过程中生成了一个XPathParser解析器的实例。

XPathParser解析器我们在前文中提到过,它使用SAX解析出输入流对应的XML的DOM树,并存放到XpathParser对象中。

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver())/*构建一个XPath解析器*/,
            configuration,/*Mybatis配置*/
            resource/*资源路径*/,
            sqlFragments/*现存的Sql代码块*/
    );
}

之后继续调用自身的其他构造方法来完成整个XmlMapperBuilder对象的初始化过程:

 private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        super(configuration);
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);/*创建Mapper文件解析助手*/
        this.parser = parser;
        this.sqlFragments = sqlFragments;
        this.resource = resource;
    }

MapperBuilderAssistant是用来解析mapper文件的工具助手,他和XmlMapperBuilder对象责任分离,XmlMapperBuilder对象解析出的数据大部分都会传递给MapperBuilderAssistant对象使用。

XmlMapperBuilder对象负责解析mapper文件,MapperBuilderAssistant对象则负责加载处理使用解析出的数据。

所以在一定程度上来讲XmlMapperBuilder对象专注于解析mapper文件获取数据,MapperBuilderAssistant对象专注于和mybatis容器的交互。

完成XmlMapperBuilder的构造工作之后,Mybatis开始调用XmlMapperBuilderparse()方法对mapper文件进行解析。

/*解析该XML文件*/
mapperParser.parse();

parse()方法中,首先调用Configuration对象的isResourceLoaded(String resource)方法校验指定的资源是否已经被处理过,如果没有处理过将会对该文件执行完全的解析过程。

Configuration对象的Set<String> loadedResources属性用来维护所有已经加载过的资源集合,主要目的是为了防止重复解析mapper文件。

如果指定的mappe文件之前未被处理过,那么Mybatis将会使用XPathParser解析器将mapper文件的mapper根元素下的所有内容解析成XNode对象实体,并交给configurationElement()方法来解析XNode对象,将解析来的数据注册到Configuration对象中,以待使用。

/**
 * MapperXml文件的解析入口方法
 */
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 配置文件为第一次加载时才会执行完整的解析操作

        // 读取并配置MapperXml文件的内容 核心逻辑
        configurationElement(parser.evalNode("/mapper"));

        // 记录已加载当前的配置文件
        configuration.addLoadedResource(resource);
        // 绑定DAO操作接口和当前配置的关系
        bindMapperForNamespace();
    }

    // 解析未完成处理的ResultMap
    parsePendingResultMaps();
    // 解析未完成处理的缓存引用
    parsePendingCacheRefs();
    // 解析未完成处理的语句
    parsePendingStatements();
}

完成映射器对象的配置工作之后,bindMapperForNamespace()方法会尝试将这些配置和一个具体的Mapper对象关联起来。

 /**
 * 绑定Mapper和命名空间的关系,这里的mapper代指DAO操作接口
 */
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 加载Mapper文件对应的Dao操作接口
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!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#loadXmlResourceloadXmlResource
                // 注册已加载过的资源集合
                configuration.addLoadedResource("namespace:" + namespace);
                // 注册DAO操作接口
                configuration.addMapper(boundType);
            }
        }
    }
}

如果指定的Mapper对象尚未解析,那么会调用ConfigurationaddMapper()方法根据Mapper对象的类型定义完成Mapper对象的初始化工作。

bindMapperForNamespace()

@startuml
hide footbox
participant  "XMLMapperBuilder" as  x
participant  "MapperBuilderAssistant" as  m
participant  "Resources" as  r
participant  "Configuration" as  c

[-> x : bindMapperForNamespace()\n绑定Mapper和命名空间的关系,这里的mapper代指DAO操作接口
activate x
x -> m ++:getCurrentNamespace() 获取命名空间
return 当前配置的命名空间
opt 已配置命名空间
x -> r++: classForName() \n 加载mapper文件对应的Mapper接口类型定义
return Mapper接口类型定义
opt 能够根据命名空间找到Mapper接口类型定义
x -> c++ :hasMapper() 判断当前是否已加载该Mapper
return 是否加载了该Mapper
opt 未加载Mapper
x-> c++: addLoadedResource() \n 注册已加载过的资源集合 ,添加namespace:前缀,防止重复加载
return
x->c++:addMapper() \n 注册Mapper接口
end
end
end
[<- x : 完成一个mapper文件的解析
@enduml

bindMapperForNamespace()
最后parse()方法会尝试依次调用parsePendingResultMaps()parsePendingCacheRefs()parsePendingCacheRefs()方法来补偿之前因为跨Mapper配置导致未完成解析的一些数据。

parse方法整体流程是:

@startuml
hide footbox
participant  XMLMapperBuilder as x
participant  Configuration as c

[-> x : parse() \n MapperXml文件的解析入口方法
activate x
x->c++:isResourceLoaded() \n 是否已加载当前资源配置文件
return
opt 未加载该资源配置文件
x->x++:configurationElement() \n读取并配置mapper文件的内容,核心逻辑
return
x->c ++:addLoadedResource() \n记录已加载当前的配置文件,防止重复加载
return
x->x++:bindMapperForNamespace() \n 绑定Mapper接口和当前配置的关系
return
end
x->x++:parsePendingResultMaps()\n解析未完成处理的ResultMap
return
x->x++:parsePendingCacheRefs()\n解析未完成处理的缓存引用
return
x->x++:parsePendingStatements()\n解析未完成处理的语句
return
[<- x : 完成一个mapper文件的解析
@enduml

parse()
上面就是XmlMapperBuilder的整体解析流程,接下来,我们将深入到每一个元素的解析过程中,来理解完整详细的Mapper依赖的数据的构建过程。

关注我,一起学习更多知识

关注我