负责解析mapper文件的XmlMapperBuilder
XmlMapperBuilder是BaseBuilder的一个实现,他的作用主要是通过解析Mybatis的mapper文件来完成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文件的定义。
Mybatis的mapper文件的根元素为mapper,他有一个必填的namespace属性,正常来说,namespace属性的取值是当前mapper文件对应的Mapper接口定义的全限定名称。
namespace属性定义的命名空间的值在整个Mybatis中应该是唯一的,通过namespace,mybatis对数据在逻辑上进行了隔离,确保不同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开始调用XmlMapperBuilder的parse()方法对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对象尚未解析,那么会调用Configuration的addMapper()方法根据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
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
XmlMapperBuilder的整体解析流程,接下来,我们将深入到每一个元素的解析过程中,来理解完整详细的Mapper依赖的数据的构建过程。