本系列文章: ??Mybatis(一)Mybatis的基本使用 ??Mybatis(二)Mybatis的高级使用 ??Mybatis(三)配置文件解析流程 ??Mybatis(四)映射文件解析流程 ??Mybatis(五)SQL执行流程 ??Mybatis(六)数据源、缓存机制、插件机制
??与配置文件不同,映射文件用于配置SQL语句,字段映射关系等。映射文件中包含<cache> 、<cache-ref> 、<resultMap> 、<sql> 、<select|insert|update|delete>等二级节点,这些节点将在接下来内容中进行分析。除了分析常规的 XML 解析过程外,还会介绍Mapper接口的绑定过程,以及其他一些知识。
1、映射文件解析解析入口
??映射文件的解析过程是配置文件解析过程的一部分,MyBatis会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement方法中:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
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");
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) {
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) {
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)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
??映射文件解析入口逻辑包含三个核心操作:
- 解析 mapper 节点。
- 通过命名空间绑定 Mapper 接口。
- 处理未完成解析的节点。
2、解析映射文件
??映射文件包含多种二级节点 , 比如<cache> , <resultMap> , <sql> 以 及<select|insert|update|delete> 等。除此之外,还包含了一些三级节点,比如 <include> ,<if> ,<where> 等。先来看一个映射文件配置示例:
<mapper namespace="xyz.test.dao.AuthorDao">
<cache/>
<resultMap id="authorResult" type="Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>
<sql id="table">
author
</sql>
<select id="findOne" resultMap="authorResult">
SELECT
id, name, age, sex, email
FROM
<include refid="table"/>
WHERE
id = #{id}
</select>
</mapper>
??以上配置中每种节点的解析逻辑都封装在了相应的方法中,这些方法由XMLMapperBuilder类的configurationElement方法统一调用:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
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"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
??在阅读源码时,按部就班地分析每个方法调用即可。不过接下来在叙述的过程中会对分析顺序进行一些调整,本章将会先分析<cache> 节点的解析过程,然后再分析<cache-ref> 节点,之后会按照顺序分析其他节点的解析过程。
2.1 解析< cache >节点
??MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启 。如果无特殊要求,二级缓存的配置很简单。
<cache/>
??如果想修改缓存的一些属性,可以像下面这样配置:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
??上面配置的意思是:
- 按先进先出的策略淘汰缓存项。
- 缓存的容量为512个对象引用。
- 缓存每隔60秒刷新一次。
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象。
??当然,除了使用Mybatis自带的缓存,也可以使用第三方缓存,比如Ehcache:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<property name="maxEntriesLocalHeap" value="1000"/>
<property name="maxEntriesLocalDisk" value="10000000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
??缓存配置的解析逻辑在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);
}
}
??上面代码中,大段代码用来解析<cache> 节点的属性和子节点,缓存对象的构建逻辑封装在 MapperBuilderAssistant类的useNewCache方法中:
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.addCache(cache);
currentCache = cache;
return cache;
}
??接下来看下Cache 实例构建过程,在CacheBuilder中实现:
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
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()) {
decorators.add(LruCache.class);
}
}
}
??以上代码主要做的事情是在implementation为空的情况下,为它设置一个默认值。其实在调用setDefaultImplementations方法之前已经进行了很多非空判断,implementation不可能为空,setDefaultImplementations 方法似乎没有存在的必要了。其实不然,如果有人不按套路写代码。比如:
Cache cache = new CacheBuilder(currentNamespace)
.build();
??忘记设置implementation ,或人为的将implementation设为空。如果不对implementation进行判空,会导致build方法在构建实例时触发空指针异常,对于框架来说,这是一个低级错误。这种情况一定要避免,以提高框架的健壮性。 ??接下来看下,如果使用第三方缓存时,其对应的配置是如何设置到缓存实例中的。
private void setCacheProperties(Cache cache) {
if (properties != null) {
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
if (metaCache.hasSetter(name)) {
Class<?> type = metaCache.getSetterType(name);
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
metaCache.setValue(name, Integer.valueOf(value));
} else if (long.class == type
|| Long.class == type) {
metaCache.setValue(name, Long.valueOf(value));
} else if (short.class == type
|| Short.class == type) {
metaCache.setValue(name, Short.valueOf(value));
} else if (byte.class == type
|| Byte.class == type) {
metaCache.setValue(name, Byte.valueOf(value));
} else if (float.class == type
|| Float.class == type) {
metaCache.setValue(name, Float.valueOf(value));
} else if (boolean.class == type
|| Boolean.class == type) {
metaCache.setValue(name, Boolean.valueOf(value));
} else if (double.class == type
|| Double.class == type) {
metaCache.setValue(name, Double.valueOf(value));
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
if (InitializingObject.class.isAssignableFrom(cache.getClass())) {
try {
((InitializingObject) cache).initialize();
} catch (Exception e) {
throw new CacheException("Failed cache initialization for '"
+ cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
}
}
}
??上面的大段代码用于对属性值进行类型转换,和设置转换后的值到Cache实例中。 ??最后,看一下设置标准装饰器的过程。
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
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> 节点为命名空间配置参照缓存,示例:
<mapper namespace="xyz.coolblog.dao.Mapper1">
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
</mapper>
??cache-ref的解析过程还是在XMLMapperBuilder中实现的:
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);
}
}
}
??<cache-ref> 节点的解析逻辑封装在了CacheRefResolver的resolveCacheRef方法中:
public Cache resolveCacheRef() {
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;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
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(List<XNode> list) {
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) {
return resultMapElement(resultMapNode, Collections.emptyList(), null);
}
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
??上面的代码做的事情:
- 获取
<resultMap> 节点的各种属性。 - 遍历
<resultMap> 的子节点,并根据子节点名称执行相应的解析逻辑。 - 构建 ResultMap 对象。
- 若构建过程中发生异常,则将resultMapResolver添加到incompleteResultMaps 集合中。
??第2步和第3步分别是<resultMap> 节点的子节点解析过程,以及ResultMap对象的构建过程,这两个过程比较重要。
- 1、 解析< id >和< result >节点
??在<resultMap> 节点中,子节点<id> 和<result> 都是常规配置,这两个节点的解析是在XMLMapperBuilder中进行的:
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
String 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");
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"));
Class<?> javaTypeClass = resolveClass(javaType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
??上面的代码主要用于获取<id> 和<result> 节点的属性。resultMap属性的解析过程要相对复杂一些。该属性存在于<association> 和<collection> 节点中。下面以<association> 节点为例,演示该节点的两种配置方式。 ??第一种配置方式是通过resultMap属性引用其他的<resultMap> 节点:
<resultMap id="articleResult" type="Article">
<id property="id" column="id"/>
<result property="title" column="article_title"/>
<association property="article_author" column="article_author_id"
resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</resultMap>
??第二种配置方式是采取resultMap嵌套的方式进行配置:
<resultMap id="articleResult" type="Article">
<id property="id" column="id"/>
<result property="title" column="article_title"/>
<association property="article_author" javaType="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</association>
</resultMap>
??<association> 的子节点是一些结果映射配置,这些结果配置最终也会被解析成ResultMap。解析过程仍在XMLMapperBuilder中:
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
if (Arrays.asList("association", "collection", "case").contains(context.getName())
&& context.getStringAttribute("select") == null) {
validateCollection(context, enclosingType);
ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
return resultMap.getId();
}
return null;
}
??<association> 的子节点由resultMapElement方法解析成ResultMap,并在最后返回resultMap.id。对于<resultMap> 节点,id的值配置在该节点的id属性中。但<association> 节点无法配置id属性,那么该id如何产生的呢?答案在XNode类的getValueBasedIdentifier方法中。 ??接下来分析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<ResultFlag> flags,
String resultSet,String foreignColumn,boolean lazy) {
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
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() {
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
resolveTypeHandler();
validate();
return resultMapping;
}
??ResultMapping的构建过程不是很复杂,首先是解析javaType类型,并创建typeHandler实例。然后处理复合column。最后通过建造器构建ResultMapping实例。
- 2、解析< constructor >节点
??有时开发时会用到特殊些的POJO,比如把POJO的setter方法移除,增加构造方法用于初始化成员变量。对于这种不可变的Java类,需要通过带有参数的构造方法进行初始化(反射也可以达到同样目的)。此时会用到<constructor> 节点,示例:
<constructor>
<idArg column="id" name="id"/>
<arg column="title" name="title"/>
<arg column="content" name="content"/>
</constructor>
??该节点的解析是在XMLMapperBuilder中的:
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<>();
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
??首先是获取并遍历子节点列表,然后为每个子节点创建flags集合,并添加CONSTRUCTOR标志。对于idArg节点,额外添加ID标志。最后一步则是构建ResultMapping。
- 3、ResultMap 对象构建过程分析
??通过前面的分析,可知<id> 、<result> 等节点最终都被解析成了ResultMapping。在得到这些ResultMapping后,紧接着要做的事情是构建ResultMap。 ??ResultMap构建的入口:
private ResultMap resultMapElement(XNode resultMapNode,
List<ResultMapping> additionalResultMappings) throws Exception {
ResultMapResolver resultMapResolver = new ResultMapResolver(
builderAssistant, id, typeClass, extend, discriminator,
resultMappings, autoMapping);
try {
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<ResultMapping> resultMappings,Boolean autoMapping) {
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<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
extendedResultMappings.removeAll(resultMappings);
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
resultMappings.addAll(extendedResultMappings);
}
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<String> constructorArgNames = new ArrayList<>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
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) {
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));
}
}
}
final String property = resultMapping.getProperty();
if (property != null) {
resultMap.mappedProperties.add(property);
}
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
resultMap.constructorResultMappings.add(resultMapping);
if (resultMapping.getProperty() != null) {
constructorArgNames.add(resultMapping.getProperty());
}
} else {
resultMap.propertyResultMappings.add(resultMapping);
}
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
resultMap.idResultMappings.add(resultMapping);
}
}
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
if (!constructorArgNames.isEmpty()) {
final List<String> 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 | 用于存储 <id> 、<result> 、<idArg> 、<arg> 节点column属性 | mappedProperties | 用于存储 <id> 和 <result> 节点的property属性,或 <idArgs> 和 <arg> 节点的name属性 | idResultMappings | 用于存储 <id> 和 <idArg> 节点对应的ResultMapping对象 | propertyResultMappings | 用于存储 <id> 和 <result> 节点对应的ResultMapping对象 | constructorResultMappings | 用于存储 <idArgs> 和 <arg> 节点对应的ResultMapping对象 |
2.4 解析< sql >节点
??<sql> 节点用来定义一些可重用的SQL语句片段,比如表名,或表的列名等。在映射文件中,我们可以通过<include> 节点引用<sql> 节点定义的内容。示例:
<sql id="table">
article
</sql>
<select id="findOne" resultType="Article">
SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
</select>
??<sql> 解析的过程还是从XMLMapperBuilder中开始:
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
??第一次传入具体的databaseId,用于解析带有databaseId属性,且属性值与此相等的<sql> 节点。第二次传入的databaseId为空,用于解析未配置databaseId属性的<sql> 节点。
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
??上面的逻辑:首先是获取<sql> 节点的id和databaseId属性,然后为id属性值拼接命名空间。最后,通过检测当前databaseId和requiredDatabaseId是否一致,来决定保存还是忽略当前的<sql> 节点。接下来看databaseId的匹配逻辑:
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
return requiredDatabaseId.equals(databaseId);
}
if (databaseId != null) {
return false;
}
if (!this.sqlFragments.containsKey(id)) {
return true;
}
XNode context = this.sqlFragments.get(id);
return context.getStringAttribute("databaseId") == null;
}
??databaseId 的匹配规则:
- databaseId与requiredDatabaseId不一致,返回 false。
- 当前节点与之前的节点出现id重复的情况,若之前的
<sql> 节点databaseId属性不为空,返回false。 - 若以上两条规则均匹配失败,此时返回true。
??。databaseId用于标明数据库厂商的身份,不同厂商有自己的 SQL 方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在<sql> 节点中有什么用呢?这个问题也不难回答。<sql> 节点用于保存 SQL 语句片段,如果 SQL 语句片段中包含方言的话,那么该<sql> 节点只能被同一databaseId 的查询语句或更新语句引用。
2.5 解析 SQL 语句节点
??前面分析了<cache> 、<cache-ref> 、<resultMap> 以及<sql> 节点,从这一节开始,我们来分析映射文件中剩余的几个节点,分别是<select> 、<insert> 、<update> 以及<delete> 等。这几个节点中存储的是相同的内容,都是SQL语句,所以这几个节点的解析过程也是相同的。
??在进行代码分析之前,这里需要特别说明一下:为了避免和<sql> 节点混淆,同时也为了描述方便,这里把<select> 、<insert> 、<update> 以及<delete> 等节点统称为SQL语句节点。
??解析过程依旧从XMLMapperBuilder开始:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
??顺着代码继续看XMLStatementBuilder:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
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 = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
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");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
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");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
??上面的代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以上代码做的事情:
- 解析
<include> 节点。 - 解析
<selectKey> 节点。 - 解析SQL,获取SqlSource。
- 构建MappedStatement实例。
- 1、解析< include >节点
??<include> 节点的解析逻辑封装在 XMLIncludeTransformer的applyIncludes方法 中:
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
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())) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
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);
}
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);
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()) {
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
- 2、解析< selectKey >节点
??对于一些不支持自增主键的数据库来说,我们在插入数据时,需要明确指定主键数据。以Oracle数据库为例,Oracle数据库不支持自增主键,但它提供了自增序列工具。<selectKey> 可以用来获取主键值。
<insert id="saveAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select author_seq.nextval from dual
</selectKey>
insert into Author
(id, name, password)
values
(#{id}, #{username}, #{password})
</insert>
??在上面的配置中,查询语句会先于插入语句执行,这样我们就可以在插入时获取到主键的值。该节点的解析是从XMLStatementBuilder中开始的:
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
removeSelectKeyNodes(selectKeyNodes);
}
??<selectKey> 节点在解析完成后,会被从dom树中移除。这样后续可以更专注的解析<insert> 或<update> 节点中的SQL,无需再额外处理<selectKey> 节点。继续跟踪:
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
for (XNode nodeToHandle : list) {
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
String databaseId = nodeToHandle.getStringAttribute("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 = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
??以上代码比较重要的一些步骤:
- 创建SqlSource实例。
- 构建并缓存MappedStatement实例。
- 构建并缓存SelectKeyGenerator实例。
- 3、解析SQL语句
??前面分析了<include> 和<selectKey> 节点的解析过程,这两个节点解析完成后,都会以不同的方式从dom树中消失。所以目前的SQL语句节点由一些文本节点和普通节点组成,比如<if> 、<where> 等。下面我们来看一下移除掉<include> 和<selectKey> 节点后的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() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource 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<SqlNode> 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 {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
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<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
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 statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
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 {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
}
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
??接着继续追踪到Configuration:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
??接着继续追踪到MapperRegistry:
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
??Mapper 接口的绑定过程,这里简单总结一下:
- 获取命名空间,并根据命名空间解析mapper类型。
- 将type和MapperProxyFactory实例存入knownMappers中。
- 解析注解中的信息。
4、处理未完成解析的节点
??在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis的做法是抛出IncompleteElementException。外部逻辑会捕捉这个异常,并将节点对应的解析器放入incomplet*集合中。先看XMLMapperBuilder:
public void parse() {
configurationElement(parser.evalNode("/mapper"));
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
??parse方法是映射文件的解析入口。上面三个以parsePending开头的方法逻辑一致,所以下面我只会分析其中一个方法的源码。简单起见,选择分析parsePendingCacheRefs的源码。先看个<cache-ref> 节点无法完成解析的例子:
<mapper namespace="xyz.coolblog.dao.Mapper1">
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
</mapper>
??假设MyBatis先解析映射文件1,然后再解析映射文件2。按照这样的解析顺序,映射文件1 中的<cache-ref> 节点就无法完成解析,因为它所引用的缓存还未被解析。当映射文件2解析完成后,MyBatis会调用parsePendingCacheRefs方法处理在此之前未完成解析的<cache-ref> 节点。
private void parsePendingCacheRefs() {
Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
synchronized (incompleteCacheRefs) {
Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
while (iter.hasNext()) {
try {
iter.next().resolveCacheRef();
iter.remove();
} catch (IncompleteElementException e) {
/从集合中删除
}
}
}
}
??上面的逻辑比较简单,这里简单总结一下:
- 获取获取 CacheRefResolver 列表,并进行遍历。
- 尝试解析
<cache-ref> 节点,若解析失败再次抛出异常。 - 若解析成功则列表中移除相关节点。
|