- 1、映射文件解析解析入口
- 2、解析映射文件
- 2.1 解析< cache >节点
- 2.2 解析< cache-ref >节点
- 2.3 解析< resultMap >节点
- 2.4 解析< sql >节点
- 2.5 解析 SQL 语句节点
- 3、Mapper接口绑定过程
- 4、处理未完成解析的节点
本系列文章:
Mybatis(一)Mybatis的基本使用
Mybatis(二)Mybatis的高级使用
Mybatis(三)配置文件解析流程
Mybatis(四)映射文件解析流程
Mybatis(五)SQL执行流程
Mybatis(六)数据源、缓存机制、插件机制
与配置文件不同,映射文件用于配置SQL语句,字段映射关系等。映射文件中包含
映射文件的解析过程是配置文件解析过程的一部分,MyBatis会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement方法中:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//获取节点中的name属性
String mapperPackage = child.getStringAttribute("name");
//从指定包中查找mapper接口,并根据mapper接口解析映射配置
configuration.addMappers(mapperPackage);
} else {
// 获取resource、url、class等属性
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);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析映射文件
mapperParser.parse();
//url 不为空,且其他两者为空,则通过url加载配置
} else
if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
//解析映射文件
mapperParser.parse();
//mapperClass不为空,且其他两者为空,则通过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.");
}
}
}
}
}
代码的主要逻辑是遍历mappers的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以XML为载体的配置称为映射文件。
在MyBatis中,共有四种加载映射文件或映射信息的方式:
- 从文件系统中加载映射文件;
- 通过URL的方式加载映射文件;
- 通过mapper接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中;
- 通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
在 MyBatis中,通过注解配置映射信息的方式是有一定局限性的,这一点MyBatis官方文档中描述的比较清楚:
最初设计时,MyBatis是一个XML驱动的框架。配置信息是基于XML的,而且映射语句也是定义在XML中的。而到了MyBatis3,就有新选择了。MyBatis3构建在全面且强大的基于Java语言的配置API之上。这个配置API是基于XML的MyBatis配置的基础,也是新的基于注解配置的基础。注解提供了一种简单的方式来实现简单映射语句,而不会引入大量的开销。
注意:Java注解的表达力和灵活性十分有限。尽管很多时间都花在调查、设计和试验上,最强大的MyBatis映射并不能用注解来构建。
可以看出:限于Java注解的表达力和灵活性,通过注解的方式并不能完全发挥MyBatis的能力。因此,对于一些较为复杂的配置信息,还是应该通过XML 的方式进行配置。
下面开始分析映射文件的解析过程,在分析之前,先来看一下映射文件解析入口。在上面的mapperElement方法中调用了mapperParser.parse()方法,这就是我们想要的入口,即XMLMapperBuilder中的parse方法:
public void parse() {
//检测映射文件是否已经被解析过
if (!configuration.isResourceLoaded(resource)) {
//解析mapper节点
configurationElement(parser.evalNode("/mapper"));
//添加资源路径到“已解析资源集合”中
configuration.addLoadedResource(resource);
//通过命名空间绑定Mapper接口
bindMapperForNamespace();
}
//处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
映射文件解析入口逻辑包含三个核心操作:
2、解析映射文件
- 解析 mapper 节点。
- 通过命名空间绑定 Mapper 接口。
- 处理未完成解析的节点。
映射文件包含多种二级节点 , 比如
author
以上配置中每种节点的解析逻辑都封装在了相应的方法中,这些方法由XMLMapperBuilder类的configurationElement方法统一调用:
private void configurationElement(XNode context) {
try {
//获取mapper命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置命名空间到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"));
//解析
在阅读源码时,按部就班地分析每个方法调用即可。不过接下来在叙述的过程中会对分析顺序进行一些调整,本章将会先分析
MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。如果无特殊要求,二级缓存的配置很简单。
如果想修改缓存的一些属性,可以像下面这样配置:
上面配置的意思是:
- 按先进先出的策略淘汰缓存项。
- 缓存的容量为512个对象引用。
- 缓存每隔60秒刷新一次。
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象。
当然,除了使用Mybatis自带的缓存,也可以使用第三方缓存,比如Ehcache:
缓存配置的解析逻辑在XMLMapperBuilder中实现:
private void cacheElement(XNode context) {
if (context != null) {
//获取各种属性
String type = context.getStringAttribute("type", "PERPETUAL");
Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
//获取子节点配置
Properties props = context.getChildrenAsProperties();
//构建缓存对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
上面代码中,大段代码用来解析
public Cache useNewCache(Class extends Cache> typeClass,
Class extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//使用建造模式构建缓存实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//添加缓存到Configuration对象中
configuration.addCache(cache);
//设置currentCache遍历,即当前使用的缓存
currentCache = cache;
return cache;
}
接下来看下Cache 实例构建过程,在CacheBuilder中实现:
public Cache build() {
//设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
setDefaultImplementations();
//通过反射创建缓存
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
//仅对内置缓存PerpetualCache应用装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
//遍历装饰器集合,应用装饰器
for (Class extends Cache> decorator : decorators) {
//通过反射创建装饰器实例
cache = newCacheDecoratorInstance(decorator, cache);
//设置属性值到缓存实例中
setCacheProperties(cache);
}
//应用标准的装饰器,比如 LoggingCache、SynchronizedCache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
//应用具有日志功能的缓存装饰器
cache = new LoggingCache(cache);
}
return cache;
}
上面的代码可以分为4步:
- 设置默认的缓存类型及装饰器。
- 应用装饰器到PerpetualCache对象上。
- 应用标准装饰器。
- 对非LoggingCache类型的缓存应用LoggingCache装饰器。
最后一步的逻辑很简单,面按顺序分析前 3 个步骤:
private void setDefaultImplementations() {
if (implementation == null) {
//设置默认的缓存实现类
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
//添加LruCache装饰器
decorators.add(LruCache.class);
}
}
}
以上代码主要做的事情是在implementation为空的情况下,为它设置一个默认值。其实在调用setDefaultImplementations方法之前已经进行了很多非空判断,implementation不可能为空,setDefaultImplementations 方法似乎没有存在的必要了。其实不然,如果有人不按套路写代码。比如:
Cache cache = new CacheBuilder(currentNamespace) //忘记设置implementation .build();
忘记设置implementation ,或人为的将implementation设为空。如果不对implementation进行判空,会导致build方法在构建实例时触发空指针异常,对于框架来说,这是一个低级错误。这种情况一定要避免,以提高框架的健壮性。
接下来看下,如果使用第三方缓存时,其对应的配置是如何设置到缓存实例中的。
private void setCacheProperties(Cache cache) {
if (properties != null) {
//为缓存实例生成一个“元信息”实例,forObject方法调用层次比较深,
//但最终调用了MetaClass的forClass方法。
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Map.Entry
上面的大段代码用于对属性值进行类型转换,和设置转换后的值到Cache实例中。
最后,看一下设置标准装饰器的过程。
private Cache setStandardDecorators(Cache cache) {
try {
//创建“元信息”对象
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
//设置size属性
metaCache.setValue("size", size);
}
if (clearInterval != null) {
//clearInterval不为空,应用ScheduledCache装饰器
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
//readWrite为true,应用SerializedCache装饰器
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
//blocking为true,应用BlockingCache装饰器
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
以上代码用于为缓存应用一些基本的装饰器,除了LoggingCache和SynchronizedCache这两个是必要的装饰器,其他的装饰器应用与否,取决于用户的配置。
2.2 解析< cache-ref >节点 在MyBatis中,二级缓存是可以共用的。这需要通过
cache-ref的解析过程还是在XMLMapperBuilder中实现的:
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
//创建CacheRefResolver实例
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//解析参照缓存
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//捕捉IncompleteElementException异常,并将cacheRefResolver
//存入到Configuration的incompleteCacheRefs集合中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
public Cache resolveCacheRef() {
//调用builderAssistant的useNewCache(namespace)方法
return assistant.useCacheRef(cacheRefNamespace);
}
上述代码又调用了MapperBuilderAssistant中的useCacheRef方法:
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
//设置cache为当前使用缓存
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
2.3 解析< resultMap >节点
resultMap是MyBatis框架中常用的特性,主要用于映射结果。
resultMap配置的解析过程也是在XMLMapperBuilder中实现的:
private void resultMapElements(Listlist) { //遍历 节点列表 for (XNode resultMapNode : list) { try { //解析resultMap节点 resultMapElement(resultMapNode); } catch (IncompleteElementException e) { } } } private ResultMap resultMapElement(XNode resultMapNode) { //调用重载方法 return resultMapElement(resultMapNode, Collections.emptyList(), null); } private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class> enclosingType) { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); //获取type属性 String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); //解析type属性对应的类型 Class> typeClass = resolveClass(type); if (typeClass == null) { typeClass = inheritEnclosingType(resultMapNode, enclosingType); } Discriminator discriminator = null; List resultMappings = new ArrayList<>(additionalResultMappings); //获取并遍历 的子节点列表 List resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { if ("constructor".equals(resultChild.getName())) { //解析constructor节点,并生成相应的ResultMapping processConstructorElement(resultChild, typeClass, resultMappings); } else if ("discriminator".equals(resultChild.getName())) { //解析discriminator节点 discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); } else { List flags = new ArrayList<>(); if ("id".equals(resultChild.getName())) { //添加ID到flags集合中 flags.add(ResultFlag.ID); } //解析id和property节点,并生成相应的ResultMapping resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } //获取id属性 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); //获取extends和autoMapping String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { //根据前面获取到的信息构建ResultMap对象 return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
上面的代码做的事情:
- 获取
节点的各种属性。 - 遍历
的子节点,并根据子节点名称执行相应的解析逻辑。 - 构建 ResultMap 对象。
- 若构建过程中发生异常,则将resultMapResolver添加到incompleteResultMaps 集合中。
第2步和第3步分别是
- 1、 解析< id >和< result >节点
在节点中,子节点 和 都是常规配置,这两个节点的解析是在XMLMapperBuilder中进行的:
private ResultMapping buildResultMappingFromContext(XNode context, Class> resultType, Listflags) { String property; //根据节点类型获取name或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"); //解析resultMap属性,该属性出现在和 节点中。 //若这两个节点不包含resultMap属性,则调用processNestedResultMappings方法 //解析嵌套resultMap。 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")); //解析javaType、typeHandler 的类型以及枚举类型JdbcType Class> javaTypeClass = resolveClass(javaType); Class extends TypeHandler>> typeHandlerClass = resolveClass(typeHandler); JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); //构建ResultMapping对象 return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); }
上面的代码主要用于获取
第一种配置方式是通过resultMap属性引用其他的
第二种配置方式是采取resultMap嵌套的方式进行配置:
的子节点是一些结果映射配置,这些结果配置最终也会被解析成ResultMap。解析过程仍在XMLMapperBuilder中:
private String processNestedResultMappings(XNode context, ListresultMappings, Class> enclosingType) { //判断节点名称 if (Arrays.asList("association", "collection", "case").contains(context.getName()) && context.getStringAttribute("select") == null) { validateCollection(context, enclosingType); //resultMapElement是解析ResultMap入口方法 ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType); //返回resultMap id return resultMap.getId(); } return null; }
的子节点由resultMapElement方法解析成ResultMap,并在最后返回resultMap.id。对于
接下来分析ResultMapping的构建过程,该过程是在MapperBuilderAssistant中实现的:
public ResultMapping buildResultMapping(Class> resultType,
String property,String column,Class> javaType,
JdbcType jdbcType,String nestedSelect,String nestedResultMap,
String notNullColumn,String columnPrefix,
Class extends TypeHandler>> typeHandler,List flags,
String resultSet,String foreignColumn,boolean lazy) {
//若javaType为空,这里根据property的属性进行解析。方法中的参数说明:
// - resultType:即 中的type属性
// - property:即 中的property属性
Class> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
//解析TypeHandler
TypeHandler> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
//解析column = {property1=column1, property2=column2}的情况,
//这里会将column拆分成多个ResultMapping
List composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
//通过建造模式构建ResultMapping
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
接着调用了ResultMapping中的build方法:
public ResultMapping build() {
//将flags和composites两个集合变为不可修改集合
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
//从TypeHandlerRegistry中获取相应TypeHandler
resolveTypeHandler();
validate();
return resultMapping;
}
ResultMapping的构建过程不是很复杂,首先是解析javaType类型,并创建typeHandler实例。然后处理复合column。最后通过建造器构建ResultMapping实例。
- 2、解析< constructor >节点
有时开发时会用到特殊些的POJO,比如把POJO的setter方法移除,增加构造方法用于初始化成员变量。对于这种不可变的Java类,需要通过带有参数的构造方法进行初始化(反射也可以达到同样目的)。此时会用到节点,示例:
该节点的解析是在XMLMapperBuilder中的:
private void processConstructorElement(XNode resultChild, Class> resultType, ListresultMappings) { //获取子节点列表 List argChildren = resultChild.getChildren(); for (XNode argChild : argChildren) { List flags = new ArrayList<>(); //向flags中添加CONSTRUCTOR标志 flags.add(ResultFlag.CONSTRUCTOR); if ("idArg".equals(argChild.getName())) { //向flags中添加ID标志 flags.add(ResultFlag.ID); } //构建ResultMapping resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); } }
首先是获取并遍历子节点列表,然后为每个子节点创建flags集合,并添加CONSTRUCTOR标志。对于idArg节点,额外添加ID标志。最后一步则是构建ResultMapping。
- 3、ResultMap 对象构建过程分析
通过前面的分析,可知、 等节点最终都被解析成了ResultMapping。在得到这些ResultMapping后,紧接着要做的事情是构建ResultMap。
ResultMap构建的入口:
private ResultMap resultMapElement(XNode resultMapNode, ListadditionalResultMappings) throws Exception { //获取resultMap节点中的属性 // ... //解析resultMap对应的类型 // ... //遍历resultMap节点的子节点,构建ResultMapping对象 // ... //创建ResultMap解析器 ResultMapResolver resultMapResolver = new ResultMapResolver( builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { //根据前面获取到的信息构建ResultMap对象 return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
ResultMap的构建逻辑封装在ResultMapResolver的resolve方法中:
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend,
this.discriminator, this.resultMappings, this.autoMapping);
}
上面的方法将构建ResultMap实例的任务委托给了MapperBuilderAssistant的addResultMap:
public ResultMap addResultMap(
String id,Class> type,String extend,Discriminator discriminator,
List resultMappings,Boolean autoMapping) {
//为ResultMap的id和extend属性值拼接命名空间
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
//为拓展ResultMappings取出重复项
extendedResultMappings.removeAll(resultMappings);
boolean declaresConstructor = false;
//检测当前resultMappings集合中是否包含CONSTRUCTOR标志的元素
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
//如果当前节点中包含子节点,
//则将拓展ResultMapping 集合中的包含CONSTRUCTOR标志的元素移除
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
//将扩展resultMappings集合合并到当前resultMappings集合中
resultMappings.addAll(extendedResultMappings);
}
//构建ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
上面的方法主要用于处理resultMap节点的extend属性,extend不为空的话,这里将当前resultMappings集合和扩展resultMappings集合合二为一。随后,通过建造模式,在ResultMap中构建ResultMap实例。
public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}
resultMap.mappedColumns = new HashSet<>();
resultMap.mappedProperties = new HashSet<>();
resultMap.idResultMappings = new ArrayList<>();
resultMap.constructorResultMappings = new ArrayList<>();
resultMap.propertyResultMappings = new ArrayList<>();
final List constructorArgNames = new ArrayList<>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
//检测或节点
//是否包含select和resultMap属性
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
final String column = resultMapping.getColumn();
if (column != null) {
//将colum转换成大写,并添加到mappedColumns集合中
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
//添加属性property到mappedProperties集合中
final String property = resultMapping.getProperty();
if (property != null) {
resultMap.mappedProperties.add(property);
}
//检测当前resultMapping是否包含CONSTRUCTOR标志
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
//添加resultMapping到constructorResultMappings中
resultMap.constructorResultMappings.add(resultMapping);
//添加属性(constructor节点的name属性)到constructorArgNames中
if (resultMapping.getProperty() != null) {
constructorArgNames.add(resultMapping.getProperty());
}
} else {
//添加resultMapping到propertyResultMappings中
resultMap.propertyResultMappings.add(resultMapping);
}
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
//添加resultMapping到idResultMappings中
resultMap.idResultMappings.add(resultMapping);
}
}
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
if (!constructorArgNames.isEmpty()) {
//获取构造方法参数列表
final List actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
if (actualArgNames == null) {
throw new BuilderException("Error in result map '" + resultMap.id
+ "'. Failed to find a constructor in '"
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
+ ". There might be more info in debug log.");
}
resultMap.constructorResultMappings.sort((o1, o2) -> {
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
return paramIdx1 - paramIdx2;
});
}
//将以下这些集合变为不可修改集合
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}
以上代码主要做的事情就是将ResultMapping实例及属性分别存储到不同的集合中,仅此而已。ResultMap中定义了五种不同的集合:
| 集合名称 | 用途 |
|---|---|
| mappedColumns | 用于存储 |
| mappedProperties | 用于存储 |
| idResultMappings | 用于存储 |
| propertyResultMappings | 用于存储 |
| constructorResultMappings | 用于存储 |
article
private void sqlElement(Listlist) { if (configuration.getDatabaseId() != null) { //调用sqlElement解析 节点 sqlElement(list, configuration.getDatabaseId()); } //再次调用sqlElement,不同的是,这次调用,该方法的第二个参数为null sqlElement(list, null); }
第一次传入具体的databaseId,用于解析带有databaseId属性,且属性值与此相等的
private void sqlElement(Listlist, String requiredDatabaseId) { for (XNode context : list) { //获取id和databaseId属性 String databaseId = context.getStringAttribute("databaseId"); String id = context.getStringAttribute("id"); //id = currentNamespace + "." + id id = builderAssistant.applyCurrentNamespace(id, false); //检测当前databaseId和requiredDatabaseId是否一致 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { //将 键值对缓存到sqlFragments中 sqlFragments.put(id, context); } } }
上面的逻辑:首先是获取
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
//当前databaseId和目标databaseId不一致时,返回false
return requiredDatabaseId.equals(databaseId);
}
//如果目标databaseId为空,但当前databaseId不为空。两者不一致,返回false
if (databaseId != null) {
return false;
}
//如果当前节点的id与之前的节点重复,且先前节点
//databaseId不为空。则忽略当前节点,并返回false
if (!this.sqlFragments.containsKey(id)) {
return true;
}
XNode context = this.sqlFragments.get(id);
return context.getStringAttribute("databaseId") == null;
}
databaseId 的匹配规则:
- databaseId与requiredDatabaseId不一致,返回 false。
- 当前节点与之前的节点出现id重复的情况,若之前的
节点databaseId属性不为空,返回false。 - 若以上两条规则均匹配失败,此时返回true。
。databaseId用于标明数据库厂商的身份,不同厂商有自己的 SQL 方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在
前面分析了
在进行代码分析之前,这里需要特别说明一下:为了避免和
节点混淆,同时也为了描述方便,这里把
解析过程依旧从XMLMapperBuilder开始:
private void buildStatementFromContext(Listlist) { if (configuration.getDatabaseId() != null) { //调用重载方法构建Statement buildStatementFromContext(list, configuration.getDatabaseId()); } //调用重载方法构建Statement,requiredDatabaseId参数为空 buildStatementFromContext(list, null); } private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { //创建Statement建造类 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //解析Statement节点,并将解析结果存储到 //configuration的mappedStatements集合中 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { //解析失败,将解析器放入Configuration的incompleteStatements集合中 configuration.addIncompleteStatement(statementParser); } } }
顺着代码继续看XMLStatementBuilder:
public void parseStatementNode() {
//获取id和databaseId属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//根据databaseId进行检测
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//获取节点的名称,比如节点名称为select
String nodeName = context.getNode().getNodeName();
//根据节点名称解析SqlCommandType
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
//解析节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//解析节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
//获取KeyGenerator实例
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
//创建KeyGenerator实例
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析SQL语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//解析Statement类型,默认为PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//获取各种属性
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
//通过别名解析resultType对应的类型
Class> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
//解析ResultSetType
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//构建MappedStatement对象,并将该对象存储到
//Configuration的mappedStatements集合中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
上面的代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以上代码做的事情:
- 解析
节点。 - 解析
节点。 - 解析SQL,获取SqlSource。
- 构建MappedStatement实例。
- 1、解析< include >节点
节点的解析逻辑封装在 XMLIncludeTransformer的applyIncludes方法 中:
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
//将configurationVariables中的数据添加到variablesContext中
Optional.ofNullable(configurationVariables)
.ifPresent(variablesContext::putAll);
//调用重载方法处理节点
applyIncludes(source, variablesContext, false);
}
上面代码中创建了一个新的Properties对象,并将全局Properties添加到其中。这样做的原因是applyIncludes的重载方法会向Properties中添加新的元素,如果直接将全局Properties传给重载方法,会造成全局Properties被污染。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if ("include".equals(source.getNodeName())) {
//获取节点。若refid中包含属性占位符${},
//则需先将属性占位符替换为对应的属性值
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
//解析的子节点,并将解析结果与variablesContext融合,
//然后返回融合后的Properties。若节点的value属性中存在
//占位符${},则将占位符替换为对应的属性值
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
//如果和节点不在一个文档中,
//则从其他文档中将节点引入到所在文档中
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
//将节点替换为节点
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
//将中的内容插入到节点之前
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
//前面已经将节点的内容插入到dom中了,
//现在不需要节点了,这里将该节点从dom中移除
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
//将source节点属性中的占位符${}替换成具体的属性值
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//递归调用
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
//将文本(text)节点中的属性占位符${}替换成具体的属性值
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
- 2、解析< selectKey >节点
对于一些不支持自增主键的数据库来说,我们在插入数据时,需要明确指定主键数据。以Oracle数据库为例,Oracle数据库不支持自增主键,但它提供了自增序列工具。可以用来获取主键值。
select author_seq.nextval from dual insert into Author (id, name, password) values (#{id}, #{username}, #{password})
在上面的配置中,查询语句会先于插入语句执行,这样我们就可以在插入时获取到主键的值。该节点的解析是从XMLStatementBuilder中开始的:
private void processSelectKeyNodes(String id, Class> parameterTypeClass, LanguageDriver langDriver) {
List selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
//解析节点,databaseId不为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
//解析节点,databaseId为空
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
//将节点从dom树中移除
removeSelectKeyNodes(selectKeyNodes);
}
private void parseSelectKeyNodes(String parentId, Listlist, Class> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) { for (XNode nodeToHandle : list) { //id = parentId + !selectKey,比如saveUser!selectKey String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX; //获取 节点的databaseId属性 String databaseId = nodeToHandle.getStringAttribute("databaseId"); //匹配databaseId if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) { //解析 节点 parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId); } } } private void parseSelectKeyNode(String id, XNode nodeToHandle, Class> parameterTypeClass, LanguageDriver langDriver, String databaseId) { //获取各种属性 String resultType = nodeToHandle.getStringAttribute("resultType"); Class> resultTypeClass = resolveClass(resultType); StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); //设置默认值 boolean useCache = false; boolean resultOrdered = false; KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; Integer fetchSize = null; Integer timeout = null; boolean flushCache = false; String parameterMap = null; String resultMap = null; ResultSetType resultSetTypeEnum = null; //创建SqlSource SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); // 节点中只能配置SELECT查询语句, //因此sqlCommandType为SqlCommandType.SELECT SqlCommandType sqlCommandType = SqlCommandType.SELECT; //构建MappedStatement,并将MappedStatement //添加到Configuration的mappedStatements map中 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); //id = namespace + "." + id id = builderAssistant.applyCurrentNamespace(id, false); MappedStatement keyStatement = configuration.getMappedStatement(id, false); //创建SelectKeyGenerator,并添加到keyGenerators map中 configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); }
以上代码比较重要的一些步骤:
- 创建SqlSource实例。
- 构建并缓存MappedStatement实例。
- 构建并缓存SelectKeyGenerator实例。
- 3、解析SQL语句
前面分析了和 节点的解析过程,这两个节点解析完成后,都会以不同的方式从dom树中消失。所以目前的SQL语句节点由一些文本节点和普通节点组成,比如 、 等。下面我们来看一下移除掉 和 节点后的SQL语句节点是如何解析的。
先从XMLLanguageDriver开始:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
接着到了XMLScriptBuilder:
public SqlSource parseScriptNode() {
//解析SQL语句节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//根据isDynamic状态创建不同的SqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
SQL语句的解析逻辑被封装在了XMLScriptBuilder类的parseScriptNode方法中。该方法首先会调用parseDynamicTags解析SQL语句节点。在解析过程中,会判断节点是是否包含一些动态标记,比如${}占位符以及动态SQL节点等。若包含动态标记,则会将isDynamic设为true。后续可根据isDynamic创建不同的SqlSource。
在XMLScriptBuilder构造方法中,会调用initNodeHandlerMap:
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
接下来继续跟踪parseDynamicTags:
protected MixedSqlNode parseDynamicTags(XNode node) {
List contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
//遍历子节点
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
//获取文本内容
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//若文本中包含${}占位符,也被认为是动态节点
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
//创建StaticTextSqlNode
contents.add(new StaticTextSqlNode(data));
}
//child节点是ELEMENT_NODE类型,比如、等
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
//获取节点名称,比如if、where、trim等
String nodeName = child.getNode().getNodeName();
//根据节点名称获取NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
//如果handler为空,表明当前节点对与MyBatis来说,是未知节点。
//MyBatis无法处理这种节点,故抛出异常
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//处理child节点,生成相应的SqlNode
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
以上方法主要是用来判断节点是否包含一些动态标记,比如${}占位符以及动态SQL节点等。这里,不管是动态SQL节点还是静态SQL节点,我们都可以把它们看成是SQL片段,一个SQL语句由多个SQL片段组成。在解析过程中,这些SQL片段被存储在contents集合中。最后,该集合会被传给MixedSqlNode构造方法,用于创建MixedSqlNode 实例。从MixedSqlNode类名上可知,它会存储多种类型的SqlNode。
SqlNode的不同实现类用于处理不同的动态SQL逻辑,这些SqlNode是由各种NodeHandler生成。之前代码中的handler.handleNode(child, contents);就是用于处理动态SQL节点,并生成相应的 SqlNode。
看一个XMLScriptBuilder中的WhereHandler例子:
private class WhereHandler implements NodeHandler {
public WhereHandler() {
}
@Override
public void handleNode(XNode nodeToHandle, List targetContents) {
//调用parseDynamicTags解析节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//创建WhereSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
//添加到targetContents
targetContents.add(where);
}
}
handleNode方法内部会再次调用parseDynamicTags解析节点中的内容,这样又会生成一个MixedSqlNode对象。最终,整个SQL语句节点会生成一个具有树状结构的MixedSqlNode。
到此,SQL语句的解析过程就分析完了。现在,我们已经将XML配置解析了SqlSource,但这还没有结束。SqlSource中只能记录SQL语句信息,除此之外,这里还有一些额外的信息需要记录。因此,需要一个类能够同时存储SqlSource和其他的信息,这个类就是MappedStatement。
- 4、构建 MappedStatement
SQL语句节点可以定义很多属性,这些属性和属性值最终存储在MappedStatement中。MappedStatement的构建过程在MapperBuilderAssistant:
public MappedStatement addMappedStatement(
String id,SqlSource sqlSource,StatementType statementType,
SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,
String parameterMap,Class> parameterType,String resultMap,
Class> resultType,ResultSetType resultSetType,boolean flushCache,
boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,
String keyProperty,String keyColumn,String databaseId,
LanguageDriver lang,String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//创建建造器,设置各种属性
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
//获取或创建ParameterMap
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
//构建MappedStatement
MappedStatement statement = statementBuilder.build();
//添加MappedStatement到configuration的mappedStatements集合中
configuration.addMappedStatement(statement);
return statement;
}
上面就是MappedStatement的构建过程。
3、Mapper接口绑定过程 映射文件解析完成后,并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定mapper接口,这样才能将映射文件中的SQL语句和mapper接口中的方法绑定在一起,后续可直接通过调用mapper接口方法执行与之对应的SQL语句。
mapper接口的绑定过程从XMLMapperBuilder开始:
private void bindMapperForNamespace() {
//获取映射文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
//根据命名空间解析mapper类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
}
//检测当前mapper类是否被绑定过
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
//绑定mapper类
configuration.addMapper(boundType);
}
}
}
接着继续追踪到Configuration:
publicvoid addMapper(Class type) { //通过MapperRegistry绑定mapper类 mapperRegistry.addMapper(type); }
接着继续追踪到MapperRegistry:
publicvoid addMapper(Class type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //将type和MapperProxyFactory进行绑定, //MapperProxyFactory可为mapper接口生成代理类 knownMappers.put(type, new MapperProxyFactory<>(type)); //创建注解解析器。在MyBatis中,有XML和注解两种配置方式可选 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); //解析注解中的信息 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
Mapper 接口的绑定过程,这里简单总结一下:
4、处理未完成解析的节点
- 获取命名空间,并根据命名空间解析mapper类型。
- 将type和MapperProxyFactory实例存入knownMappers中。
- 解析注解中的信息。
在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis的做法是抛出IncompleteElementException。外部逻辑会捕捉这个异常,并将节点对应的解析器放入incomplet*集合中。先看XMLMapperBuilder:
public void parse() {
//...
//解析mapper节点
configurationElement(parser.evalNode("/mapper"));
//处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
parse方法是映射文件的解析入口。上面三个以parsePending开头的方法逻辑一致,所以下面我只会分析其中一个方法的源码。简单起见,选择分析parsePendingCacheRefs的源码。先看个
假设MyBatis先解析映射文件1,然后再解析映射文件2。按照这样的解析顺序,映射文件1 中的
private void parsePendingCacheRefs() {
//获取CacheRefResolver列表
Collection incompleteCacheRefs = configuration.getIncompleteCacheRefs();
synchronized (incompleteCacheRefs) {
Iterator iter = incompleteCacheRefs.iterator();
//通过迭代器遍历列表
while (iter.hasNext()) {
try {
//尝试解析节点,若解析失败,则抛出
//IncompleteElementException,此时下面的删除操作不会被执行
iter.next().resolveCacheRef();
//移除CacheRefResolver对象。如果代码能执行到此处,
//表明已成功解析了节点
iter.remove();
} catch (IncompleteElementException e) {
//如果再次发生IncompleteElementException,表明当前
//映射文件中并没有所引用的缓存。有可能所引用的缓存
//在后面的映射文件中,所以这里不能将解析失败的CacheRefResolver
/从集合中删除
}
}
}
}
上面的逻辑比较简单,这里简单总结一下:
- 获取获取 CacheRefResolver 列表,并进行遍历。
- 尝试解析
节点,若解析失败再次抛出异常。 - 若解析成功则列表中移除相关节点。



