MyBaits映射器文件解析:总览
今天我们继续学习 MyBatis 解析映射器文件的源码,深入到 XMLMapperBuilder 中,对 XMLMapperBuilder#parse
方法做一个整体上的分析。
由于解析 MyBatis 映射器文件的 resultMap 元素和解析 SQL 语句(sql 元素,select 元素,insert 元素,update 元素和 delete 元素)的内容过于庞大,塞到一篇文章中实在有些困难,因此本文中会对这部分内容一笔带过,后面单独成文进行分析。
try...catch...resources 语句
正式开始前今天的内容前,我先回答私信中的一个关于 Java 语法特性的问题。XMLConfigBuilder#mappersElement
方法中有一段 try 语句块的内容,如下:
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
第 1 行代码中,try 关键字后面使用了小括号,并在其中声明 InputStream 类型的变量。可能很多小伙伴没有遇到过 try 关键字的这种用法,对它的功能是不是很了解。
“try...catch...resources”语句是 Java1.7 中引入的特性,它允许直接在 try 语句中声明一个或多个继承自 java.lang.AutoCloseable 的资源,这些资源会在 try 语句块执行完毕后自动关闭,即便是 try 语句块中发生了异常。
常见继承自 AutoCloseable 的 Java 类如下:
XMLConfigBuilder#mappersElement
方法中的这段代码等价于如下形式的“try...catch...finally”语句代码:
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} finally {
if(inputStream != null) {
inputStream.close();
}
}
对比两者的差异,最直观的便是“try...catch...resources”语句的代码量会少很多,且不需要在 try 语句块外声明变量,不过最重要的是在“try...catch...resources” 语句中,无需显示调用 AutoCloseable#close
方法,JVM 会自动调用 AutoCloseable#close
方法,这样可以有效的防止资源泄漏。
好了,下面我们回到 XMLConfigBuilder#mappersElement
方法。
MyBatis 解析映射器的方式
XMLConfigBuilder#mappersElement
方法的部分源码如下:
private void mappersElement(XNode context) throws Exception {
// 循环 MyBatis核心配置文件的 mappers 元素
for (XNode child : context.getChildren()) {
// 使用 package 元素配置的映射器
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 属性配置的映射器
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
}
/// 使用 url 属性配置的映射器
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try (InputStream inputStream = Resources.getUrlAsStream(url)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
}
// 使用 mapperClass 属性配置的映射器
else if (resource == null && url == null && mapperClass != null) {
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.");
}
}
}
}
上面的源码中,总共有 4 种解析 MyBatis 映射器文件的逻辑,总体可以归纳为两大类:
- 使用 mappers 元素的子元素 package 配置的 MyBatis 映射器和使用 mappers 元素的子元素 mapper 的 class 属性配置的 MyBatis 映射器,这两种方式最终通过调用
MapperAnnotationBuilder#parse
方法实现 MyBatis 映射器文件的解析; - 使用 mappers 元素的子元素 mapper 的 resource 属性和 url 属性配置的 MyBatis 映射器,这两种方式最终是通过调用
XMLMapperBuilder#parse
方法实现 MyBatis 映射器文件的解析。
虽然方式上有所差异,但它们最终的目的是相同的,都是为了解析 MyBatis 映射器文件,构建出 MyBatis 应用程序内部使用的实例,以实现后续 MyBatis 执行 SQL 语句的功能。
由于我们一直是以 MyBatis 原生应用程序为例进行学习的,并且使用的是 XML 形式的 MyBatis 映射器配置,因此我们下面只对 XMLMapperBuilder#parse
方法进行分析。
Tips:MapperAnnotationBuilder 主要功能是处理 MyBatis 注解配置的。
构建 XMLMapperBuilder
了解 XMLMapperBuilder#parse
方法前,我们先来看构建 XMLMapperBuilder 实例的过程,它构造方法源码如下:
public class XMLMapperBuilder extends BaseBuilder {
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;
}
}
与 XMLConfigBuilder 一样,XMLMapperBuilder 也是继承自 BaseBuilder,这也就说明每一个 XMLMapperBuilder 实例内部都存储着 Configuration 实例,TypeAliasRegistry 实例和 TypeHandlerRegistry 实例。
除了 XMLConfigBuilder 和 XMLMapperBuilder 外,MyBatiis 构建 SQL 语句使用到的 Builder 也是继承自 BaseBuilder 的,它的集成体系如下:
本文中还会出现继承自 BaseBuilder 的 MapperBuilderAssistant,至于其它的继承自 BaseBuilder 的子类,我们还会在后续的文章中看到。
XMLMapperBuilder 在构造方法中为 4 个自身独有的成员变量进行了赋值,来看下这 4 个变量的作用:
- MapperBuilderAssistant 类型的变量 builderAssistant,用于解析和注册 MyBatis 映射器的辅助类;
- XPathParser 类型的变量 parser,与 XMLConfigBuilder 中的 parser 类似,此时用于构建 XPathParser 的变量 inputStream 是由 MyBatis 映射器解析而来;
- Map<String, XNode> 类型的变量 sqlFragments,用于存储 SQL 语句的容器;
- String 类型的变量 resource,MyBatis 映射器文件的路径。
关于 MapperBuilderAssistant 的内容,我们已经在 《MyBatis中二级缓存的配置与实现原理》 中聊过了,这里就不赘述了。
XMLMapperBuilder#parse 方法解析
XMLMapperBuilder#parse
方法的源码如下:
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
// 将已经解析的资源文件存储到 Configuration 实例的 loadedResources 变量中,该变量底层使用 HasSet
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
}
XMLMapperBuilder#parse
方法中先后调用了 6 个方法:
Configuration#isResourceLoaded
方法,用于判断当前 Configuration 实例是否加载过资源文;XMLMapperBuilder#configurationElement
方法,用于处理 MyBatis 映射器文件中 mapper 元素的配置内容;XMLMapperBuilder#bindMapperForNamespace
方法,用于注册 MyBatis 映射器文件对应的 Mapper 接口;XMLMapperBuilder#parsePendingResultMaps
方法,用于处理XMLMapperBuilder#configurationElement
方法中解析失败的 resultMap 元素的配置内容;XMLMapperBuilder#parsePendingCacheRefs
方法,用于处理XMLMapperBuilder#configurationElement
方法中解析失败的 cache-ref 元素的配置内容;XMLMapperBuilder#parsePendingStatements
方法,用于处理XMLMapperBuilder#configurationElement
方法中解析失败的 SQL 语句。
Configuration#isResourceLoaded 方法解析
Configuration#isResourceLoaded
方法非常简单,用于判断当前的 Configuration 实例是否加载过资源文件,源码如下:
public class Configuration {
protected final Set<String> loadedResources = new HashSet<>();
public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}
}
实现方式上,Configuration 使用 HashSet 类型的变量 loadedResources 存储加载完成的资源文件,通过调用 Set#contains
方法判断改文件是否已经完成加载。
XMLMapperBuilder#configurationElement 方法解析
XMLMapperBuilder#configurationElement
方法负责调用解析 MyBatis 映射器中每项配置的方法,该方法的部分源码如下:
public class XMLMapperBuilder extends BaseBuilder {
private void configurationElement(XNode context) {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 将 namespace 赋值给 builderAssistant
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
}
不过这里需要注意,XMLMapperBuilder#configurationElement
方法是在 XMLConfigBuilder#mappersElement
方法的循环中调用的(跳过了“中间商”XMLMapperBuilder#parse
方法),XMLConfigBuilder#mappersElement
方法循环的是 MyBatis 核心配置文件中的 mappers 元素的子元素 mapper,即循环每个 MyBatis 映射器文件,并别调用 XMLMapperBuilder#configurationElement
方法进行解析,那么我们可以理解为 XMLMapperBuilder#configurationElement
方法的入参是每个 MyBatis 映射器,这也就是说 XMLMapperBuilder#configurationElement
方法是在每个 MyBatis 映射器对应的 namespace 下操作的。
解析 namespace 元素
XMLMapperBuilder#configurationElement
方法的第 3 行代码,获取 MyBatis 映射器文件中 mapper 元素的 namespace 属性的配置,XNode 是 MyBatis 内部的封装,底层借助了 java.util.Properties 类实现配置的读取;第 4 行代码到第 6 行代码的条件语句是对 namespace 进行的非空校验,如果 namespace 为空则直接抛出异常,这会导致 MyBatis 应用程序启动失败,这点我们在 《MyBatis 入门》 中也提到过。
解析 cache-ref 元素
XMLMapperBuilder#configurationElement
方法的第 9 行代码通过调用 XMLMapperBuilder#cacheRefElement
方法解析 MyBatis 映射器中 cache-ref 元素的配置,该方法源码如下:
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
《MyBatis中二级缓存的配置与实现原理》 中,我们已经介绍过了 cache 元素,可以知道每个 MyBatis 映射器中的 cache 元素(二级缓存)都是与它的 namespace 绑定的,并且会存储到 Configuration 的成员变量 caches 中。
cache-ref 元素负责引用其它 MyBatis 映射器定义的二级缓存(cache 元素的配置),并将关联关系保存到 Configuration 的 Map<String, String>类型的变 cacheRefMap 中。
第 3 行代码,将当前 MyBatis 映射器的 namespace 作为 Key,被引用的二级缓存对应的 MyBatis 映射器的 namespace 作为 Value,存储到 cacheRefMap 中;第 4 行代码,创建 CacheRefResolver 实例,该实例中保存了 MapperBuilderAssistant 实例和被引用的二级缓存对应的 MyBatis 映射器的 namespace;第 6 行代码,调用 CacheRefResolver#resolveCacheRef
方法,解析被引用的二级缓存,源码如下:
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
接着来看 MapperBuilderAssistant#useCacheRef
方法,部分源码如下:
public Cache useCacheRef(String namespace) {
// 标记未成功解析 cache-ref
unresolvedCacheRef = true;
// 通过 namespace获取 Cache 实例
Cache cache = configuration.getCache(namespace);
// Cache实例为空时抛出异常
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 将当前 namespace 的 currentCache 变量指向被引用的 Cache 实例
currentCache = cache;
// 标记已成功解析 cache-ref
unresolvedCacheRef = false;
return cache;
}
来看第 7 行的条件语句,此时的 namespace 为被引用的二级缓存对应的 MyBatis 映射器的 namespace,如果此时获取到的 Cache 实例为空,则说明该 namespace 所在的 MyBatis 映射器还未被解析,这时会抛出异常,并回到 XMLMapperBuilder#cacheRefElement
方法的第 8 行,将 CacheRefResolver 实例存储到 Configuration 的成员变量 incompleteCacheRefs 中。
这里我们需要关注下 Configuration 的成员变量 incompleteCacheRefs,它存储第一次解析 cache-ref 元素失败的 CacheRefResolver 实例。第一次解析会失败的原因是,由于配置顺序的不同,可能存在使用到 cache-ref 元素的 MyBatis 映射器先于被引用二级缓存的 MyBatis 映射器解析,导致解析 cache-ref 元素时,引用 Cache 实例失败。
Configuration 中除了 incompleteCacheRefs 外,Configuration 中还有 3 个用于存储未完成解析的元素的容器:
// 用于存储未完成解析的 SQL 语句
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
// 用于存储未完成解析的结果集映射(resultMap 元素)
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
// 用于存储未完成解析或未绑定到SQL语句的接口方法
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
其中 incompleteStatements 和 incompleteResultMaps 我们在后面的文章中还会涉及到,至于 incompleteMethods 是 MyBatis 注解开发时使用的,我们不会涉及到这部分内容。
解析 cache 元素
XMLMapperBuilder#configurationElement
方法的第 10 行代码调用的 XMLMapperBuilder#cacheElement
方法是用来解析 MyBatis 映射器文件中的 cache 元素的,也就是解析配置创建 MyBatis 二级缓存的过程,这部分源码我们已经在 《MyBatis中二级缓存的配置与实现原理》 中做过了分析,这里就不再赘述了,不熟悉的小伙伴可以回顾下这篇文章。
解析 parameterMap 元素
XMLMapperBuilder#configurationElement
方法的第 11 行代码调用的 XMLMapperBuilder#parameterMapElement
方法是用来解析 parameterMap 元素的,该元素已经被 MyBatis 官方标记为废弃,并且在未来的版本中可能会被移除,所以关于 parameterMap 元素的源码也不在我们分析的范畴中。
解析 resultMapMap 元素与解析 SQL 语句
XMLMapperBuilder#configurationElement
方法的第 12 行代码调用的 XMLMapperBuilder#resultMapElements
方法是负责解析 resultMap 元素的,第 13 行调用的 XMLMapperBuilder#sqlElement
方法是负责解析 sql 元素的,第 14 行调用的 XMLMapperBuilder#buildStatementFromContext
方法是负责解析 select 元素,insert 元素,update 元素和 delete 元素的,由于这部分内容非常庞大,因此这部分我们单独成文,再做分析。
其中,第 12 行调用的 XMLMapperBuilder#resultMapElements
方法和第 14 行调用的 XMLMapperBuilder#buildStatementFromContext
方法中,如果解析失败,会将解析失败的内容存储到 Configuration 实例的 incompleteResultMaps 变量和 incompleteStatements 变量中。
XMLMapperBuilder#bindMapperForNamespace 方法分析
XMLMapperBuilder#bindMapperForNamespace
方法用于实现将 MyBatis 映射器文件绑定到对应的 Mapper 接口上,并注册到 MapperRegistry 中,修改后的方法源码如下:
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
//使用反射获取 namespace 对应的 Java 接口
Class<?> boundType = Resources.classForName(namespace);
// 判断当前的 Mapper 接口是否被注册
if (boundType != null && !configuration.hasMapper(boundType)) {
// 将当前的 MyBatis 映射器添加到 Configuration 的 loadedResources 变量中,表示已经完成了当前资源文件的加载
configuration.addLoadedResource("namespace:" + namespace);
// 将 MyBatis 的映射器注册到 MapperRegistry 的 knownMappers 中
configuration.addMapper(boundType);
}
}
}
处理未完成的解析
上面我们提到,在解析 cache-ref,resultMap 元素和 SQL 语句时,可能存在部分未完成解析的情况,并将它们分别存储到 Configuration 的 incompleteCacheRefs,incompleteResultMaps 和 incompleteStatements 中。
而在 XMLMapperBuilder#parse
方法中,最后调用的 3 个方法就是为了再次处理这些未完成解析的配置:
XMLMapperBuilder#parsePendingCacheRefs
方法,解析 incompleteCacheRefs;XMLMapperBuilder#parsePendingResultMaps
方法,解析 incompleteResultMaps;XMLMapperBuilder#parsePendingStatements
方法,解析 incompleteStatements。
由于这 3 个方法除了解析的内容不同外,其余完全一致,这里我就以 XMLMapperBuilder#parsePendingCacheRefs
方法方法为例,源码如下:
private void parsePendingCacheRefs() {
// 获取 incompleteCacheRefs
Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
// 加锁
synchronized (incompleteCacheRefs) {
Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
while (iter.hasNext()) {
try {
// 重新解析 cache-ref 元素
iter.next().resolveCacheRef();
iter.remove();
} catch (IncompleteElementException e) {
}
}
}
}
逻辑上非常简单,只是遍历了 incompleteCacheRefs 中的元素,重新进行解析,解析成功后便将其从容器中删除。
文章中使用的 MyBatiis 版本为 3.5.15,这个版本中 XMLMapperBuilder#parsePendingResultMaps
方法,XMLMapperBuilder#parsePendingCacheRefs
方法和 XMLMapperBuilder#parsePendingStatements
方法使用了 synchronized 关键字进行加锁,防止并发问题。
2024 年 4 月 3 日发布 MyBatis 3.5.16 版本之后(包括尚未正式发布的 3.5.17),这 3 个方法中使用了 ReentrantLock 替代 synchronized,并使用了 Collection#removeIf
方法和 Lambda 表达式替换了迭代器(Iterator),因此如果你使用的是最新版的 MyBatiis,源码上会有所出入。
好了,到这里我们就把解析 MyBatis 映射器的整体流程介绍完了,下一篇文章中,我会和大家一起分析 MyBatis 是如何解析 resultMap 元素的。
转载自:https://juejin.cn/post/7392071369650847779