最后一个节点的解析是 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.xmlMapperBuilderAssistant 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")