淘宝联盟个人网站怎么做,网站里做个子网页怎么做,做网站给女朋友,pc网站的优势1.简介
在上一篇文章中#xff0c;我详细分析了 MyBatis 配置文件的解析过程。由于上一篇文章的篇幅比较大#xff0c;加之映射文件解析过程也比较复杂的原因。所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来#xff0c;独立成文#xff0c;于是就有了本篇文章…1.简介
在上一篇文章中我详细分析了 MyBatis 配置文件的解析过程。由于上一篇文章的篇幅比较大加之映射文件解析过程也比较复杂的原因。所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来独立成文于是就有了本篇文章。在本篇文章中我将分析映射文件中出现的一些及节点比如 , select | insert | update | delete 等。除了分析常规的 XML 解析过程外我还会向大家介绍 Mapper 接口的绑定过程等。综上所述本篇文章内容会比较丰富如果大家对此感兴趣不妨花点时间读一读会有新的收获。当然本篇文章通篇是关于源码分析的所以阅读本文需要大家对 MyBatis 有一定的了解。如果大家对 MyBatis 还不是很了解建议阅读一下 MyBatis 的官方文档。
其他的就不多说了下面开始我们的 MyBatis 源码之旅。
2.映射文件解析过程分析
我在前面说过映射文件的解析过程是 MyBatis 配置文件解析过程的一部分。MyBatis 的配置文件由 XMLConfigBuilder 的 parseConfiguration 进行解析该方法依次解析了 、、 等节点。至于 节点parseConfiguration 则是在方法的结尾对其进行了解析。该部分的解析逻辑封装在 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 官方文档中描述的比较清楚。这里引用一下 因为最初设计时MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的而且映射语句也是定义在 XML 中的。而到了 MyBatis 3就有新选择了。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。这个配置 API 是基于 XML 的 MyBatis 配置的基础也是新的基于注解配置的基础。注解提供了一种简单的方式来实现简单映射语句而不会引入大量的开销。 注意 不幸的是Java 注解的的表达力和灵活性十分有限。尽管很多时间都花在调查、设计和试验上最强大的 MyBatis 映射并不能用注解来构建——并不是在开玩笑的确是这样。 如上所示重点语句我用黑体标注了出来。限于 Java 注解的表达力和灵活性通过注解的方式并不能完全发挥 MyBatis 的能力。所以对于一些较为复杂的配置信息我们还是应该通过 XML 的方式进行配置。正因此在接下的章节中我会重点分析基于 XML 的映射文件的解析过程。如果能弄懂此种配置方式的解析过程那么基于注解的解析过程也不在话下。
下面开始分析映射文件的解析过程在展开分析之前先来看一下映射文件解析入口。如下 public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode(/mapper));configuration.addLoadedResource(resource);bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();
}
如上映射文件解析入口逻辑包含三个核心操作分别如下
解析 mapper 节点通过命名空间绑定 Mapper 接口处理未完成解析的节点
这三个操作对应的逻辑我将会在随后的章节中依次进行分析。下面先来分析第一个操作对应的逻辑。
2.1 解析映射文件
在 MyBatis 映射文件中可以配置多种节点。比如 以及 select | insert | update | delete 等。下面我们来看一个映射文件配置示例。
mapper namespacexyz.coolblog.dao.AuthorDaocache/resultMap idauthorResult typeAuthorid propertyid columnid/result propertyname columnname//resultMapsql idtableauthor/sqlselect idfindOne resultMapauthorResultSELECTid, name, age, sex, emailFROMinclude refidtable/WHEREid #{id}/select/mapper
上面是一个比较简单的映射文件还有一些的节点没有出现在上面。以上每种配置中的每种节点的解析逻辑都封装在了相应的方法中这些方法由 XMLMapperBuilder 类的 configurationElement 方法统一调用。该方法的逻辑如下
private void configurationElement(XNode context) {try {String namespace context.getStringAttribute(namespace);if (namespace null || namespace.equals()) {throw new BuilderException(Mappers 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);}
}
上面代码的执行流程清晰明了。在阅读源码时我们可以按部就班的分析每个方法调用即可。不过在写文章进行叙述时需要做一些调整。下面我将会先分析 节点的解析过程然后再分析 节点之后会按照顺序分析其他节点的解析过程。接下来我们来看看 节点的解析过程。
2.1.1 解析 节点
MyBatis 提供了一、二级缓存其中一级缓存是 SqlSession 级别的默认为开启状态。二级缓存配置在映射文件中使用者需要显示配置才能开启。如果没有特殊要求二级缓存的配置很容易。如下
cache/
如果我们想修改缓存的一些属性可以像下面这样配置。
cacheevictionFIFOflushInterval60000size512readOnlytrue/
根据上面的配置创建出的缓存有以下特点
按先进先出的策略淘汰缓存项缓存的容量为 512 个对象引用缓存每隔60秒刷新一次缓存返回的对象是写安全的即在外部修改对象不会影响到缓存内部存储对象
除了上面两种配置方式我们还可以给 MyBatis 配置第三方缓存或者自己实现的缓存等。比如我们将 Ehcache 缓存整合到 MyBatis 中可以这样配置。
cache typeorg.mybatis.caches.ehcache.EhcacheCache/property nametimeToIdleSeconds value3600/property nametimeToLiveSeconds value3600/property namemaxEntriesLocalHeap value1000/property namemaxEntriesLocalDisk value10000000/property namememoryStoreEvictionPolicy valueLRU/
/cache
以上简单介绍了几种缓存配置方式关于 MyBatis 缓存更多的知识后面我会独立成文进行分析这里就不深入说明了。下面我们来分析一下缓存配置的解析逻辑如下
private void cacheElement(XNode context) throws Exception {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);}
}
上面代码中大段代码用来解析 节点的属性和子节点这些代码没什么好说的。缓存的构建逻辑封装在 BuilderAssistant 类的 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 实例Cache 实例的构建过程略为复杂我们跟下去看看。 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;
}
上面的构建过程流程较为复杂这里总结一下。如下
设置默认的缓存类型及装饰器应用装饰器到 PerpetualCache 对象上 遍历装饰器类型集合并通过反射创建装饰器实例将属性设置到实例中 应用一些标准的装饰器对非 LoggingCache 类型的缓存应用 LoggingCache 装饰器
在以上4个步骤中最后一步的逻辑很简单无需多说。下面按顺序分析前3个步骤对应的逻辑如下
private void setDefaultImplementations() {if (implementation null) {implementation PerpetualCache.class;if (decorators.isEmpty()) {decorators.add(LruCache.class);}}
}
以上逻辑比较简单主要做的事情是在 implementation 为空的情况下为它设置一个默认值。如果大家仔细看前面的方法会发现 MyBatis 做了不少判空的操作。比如 String type context.getStringAttribute(type, PERPETUAL);
String eviction context.getStringAttribute(eviction, LRU);Cache cache new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).build();
既然前面已经做了两次判空操作implementation 不可能为空那么 setDefaultImplementations 方法似乎没有存在的必要了。其实不然如果有人不按套路写代码。比如
Cache cache new CacheBuilder(currentNamespace).build();
这里忘记设置 implementation或人为的将 implementation 设为空。如果不对 implementation 进行判空会导致 build 方法在构建实例时触发空指针异常对于框架来说出现空指针异常是很尴尬的这是一个低级错误。这里以及之前做了这么多判空就是为了避免出现空指针的情况以提高框架的健壮性。好了关于 setDefaultImplementations 方法的分析先到这继续往下分析。
我们在使用 MyBatis 内置缓存时一般不用为它们配置自定义属性。但使用第三方缓存时则应按需进行配置。比如前面演示 MyBatis 整合 Ehcache 时就为 Ehcache 配置了一些必要的属性。下面我们来看一下这部分配置是如何设置到缓存实例中的。
private void setCacheProperties(Cache cache) {if (properties ! null) {MetaObject metaCache SystemMetaObject.forObject(cache);for (Map.EntryObject, 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) {...} else if (byte.class type || Byte.class type) {...} else if (float.class type || Float.class type) {...} else if (boolean.class type || Boolean.class type) {...} else if (double.class type || Double.class type) {...} 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 实例中。关于上面代码中出现的 MetaObject大家可以自己尝试分析一下。最后我们来看一下设置标准装饰器的过程。如下
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.1.2 解析 节点
在 MyBatis 中二级缓存是可以共用的。这需要使用 节点配置参照缓存比如像下面这样。 mapper namespacexyz.coolblog.dao.Mapper1cache-ref namespacexyz.coolblog.dao.Mapper2/
/mappermapper namespacexyz.coolblog.dao.Mapper2cache/
/mapper
接下来我们对照上面的配置分析 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);}}
}
如上所示 节点的解析逻辑封装在了 CacheRefResolver 的 resolveCacheRef 方法中。下面我们一起看一下这个方法的逻辑。 public Cache resolveCacheRef() {return assistant.useCacheRef(cacheRefNamespace);
}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);}
}
以上是 cache-ref 的解析过程逻辑并不复杂。不过这里要注意 cache 为空的情况我在代码中已经注释了可能导致 cache 为空的两种情况。第一种情况比较好理解第二种情况稍微复杂点但是也不难理解。我会在 2.3 节进行解释说明这里先不说。
到此关于 节点的解析过程就分析完了。本节的内容不是很难理解就不多说了。
2.1.3 解析 节点
resultMap 是 MyBatis 框架中常用的特性主要用于映射结果。resultMap 是 MyBatis 提供的一个强力武器这一点官方文档中有所描述这里引用一下。 resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来, 并在一些情形下允许你做一些 JDBC 不支持的事情。 实际上在对复杂语句进行联合映射的时候它很可能可以代替数千行的同等功能的代码。 ResultMap 的设计思想是简单的语句不需要明确的结果映射而复杂一点的语句只需要描述它们的关系就行了。 如上描述resultMap 元素是 MyBatis 中最重要最强大的元素它可以把大家从 JDBC ResultSets 数据提取的工作中解放出来。通过 resultMap 和自动映射可以让 MyBatis 帮助我们完成 ResultSet → Object 的映射这将会大大提高了开发效率。关于 resultMap 的用法我相信大家都比较熟悉了所以这里我就不介绍了。当然如果大家不熟悉也没关系MyBatis 的官方文档上对此进行了详细的介绍大家不妨去看看。
好了其他的就不多说了下面开始分析 resultMap 配置的解析过程。 private void resultMapElements(ListXNode list) throws Exception {for (XNode resultMapNode : list) {try {resultMapElement(resultMapNode);} catch (IncompleteElementException e) {}}
}private ResultMap resultMapElement(XNode resultMapNode) throws Exception {return resultMapElement(resultMapNode, Collections.ResultMappingemptyList());
}private ResultMap resultMapElement(XNode resultMapNode, ListResultMapping additionalResultMappings) throws Exception {ErrorContext.instance().activity(processing resultMapNode.getValueBasedIdentifier());String id resultMapNode.getStringAttribute(id, resultMapNode.getValueBasedIdentifier());String type resultMapNode.getStringAttribute(type,resultMapNode.getStringAttribute(ofType,resultMapNode.getStringAttribute(resultType,resultMapNode.getStringAttribute(javaType))));String extend resultMapNode.getStringAttribute(extends);Boolean autoMapping resultMapNode.getBooleanAttribute(autoMapping);Class? typeClass resolveClass(type);Discriminator discriminator null;ListResultMapping resultMappings new ArrayListResultMapping();resultMappings.addAll(additionalResultMappings);ListXNode 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 {ListResultFlag flags new ArrayListResultFlag();if (id.equals(resultChild.getName())) {flags.add(ResultFlag.ID);}resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}}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 添加到 incompleteResultMaps 集合中
如上流程第1步和最后一步都是一些常规操作无需过多解释。第2步和第3步则是接下来需要重点分析的操作这其中鉴别器 discriminator 不是很常用的特性我觉得大家知道它有什么用就行了所以就不分析了。下面先来分析 和 节点的解析逻辑。
2.1.3.1 解析 和 节点
在 节点中子节点 和 都是常规配置比较常见。相信大家对此也比较熟悉了我就不多说了。下面我们直接分析这两个节点的解析过程。如下
private ResultMapping buildResultMappingFromContext(XNode context, Class? resultType, ListResultFlag flags) throws Exception {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.ResultMappingemptyList()));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 (Class? extends TypeHandler?) resolveClass(typeHandler);JdbcType jdbcTypeEnum resolveJdbcType(jdbcType);return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect,nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
上面的方法主要用于获取 和 节点的属性其中resultMap 属性的解析过程要相对复杂一些。该属性存在于 和 节点中。下面以 节点为例演示该节点的两种配置方式分别如下
第一种配置方式是通过 resultMap 属性引用其他的 节点配置如下
resultMap idarticleResult typeArticleid propertyid columnid/result propertytitle columnarticle_title/association propertyarticle_author columnarticle_author_id javaTypeAuthor resultMapauthorResult/
/resultMapresultMap idauthorResult typeAuthorid propertyid columnauthor_id/result propertyname columnauthor_name/
/resultMap
第二种配置方式是采取 resultMap 嵌套的方式进行配置如下
resultMap idarticleResult typeArticleid propertyid columnid/result propertytitle columnarticle_title/association propertyarticle_author javaTypeAuthorid propertyid columnauthor_id/result propertyname columnauthor_name//association
/resultMap
如上配置 的子节点是一些结果映射配置这些结果配置最终也会被解析成 ResultMap。我们可以看看解析过程是怎样的如下
private String processNestedResultMappings(XNode context, ListResultMapping resultMappings) throws Exception {if (association.equals(context.getName())|| collection.equals(context.getName())|| case.equals(context.getName())) {if (context.getStringAttribute(select) null) {ResultMap resultMap resultMapElement(context, resultMappings);return resultMap.getId();}}return null;
}
如上 的子节点由 resultMapElement 方法解析成 ResultMap并在最后返回 resultMap.id。对于 节点id 的值配置在该节点的 id 属性中。但 节点无法配置 id 属性那么该 id 如何产生的呢答案在 XNode 类的 getValueBasedIdentifier 方法中这个方法具体逻辑我就不分析了。下面直接看一下以上配置中的 节点解析成 ResultMap 后的 id 值如下
id mapper_resultMap[articleResult]_association[article_author]
关于嵌套 resultMap 的解析逻辑就先分析到这下面分析 ResultMapping 的构建过程。
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, ListResultFlag flags, String resultSet, String foreignColumn, boolean lazy) {Class? javaTypeClass resolveResultJavaType(resultType, property, javaType);TypeHandler? typeHandlerInstance resolveTypeHandler(javaTypeClass, typeHandler);ListResultMapping 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 ArrayListResultFlag() : flags).composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn)).columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).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 实例。关于上面方法中出现的一些方法调用这里接不跟下去分析了大家可以自己看看。
到此关于 ResultMapping 的解析和构建过程就分析完了总的来说还是比较复杂的。不过再难也是人写的静下心都可以看懂。好了其他就不多说了继续往下分析。
2.1.3.2 解析 一般情况下我们所定义的实体类都是简单的 Java 对象即 POJO。这种对象包含一些私有属性和相应的 getter/setter 方法通常这种 POJO 可以满足大部分需求。但如果你想使用不可变类存储查询结果则就需要做一些改动。比如把 POJO 的 setter 方法移除增加构造方法用于初始化成员变量。对于这种不可变的 Java 类需要通过带有参数的构造方法进行初始化反射也可以达到同样目的。下面举个例子说明一下 public class ArticleDO {public ArticleDO(Integer id, String title, String content) {this.id id;this.title title;this.content content;}} 如上ArticleDO 的构造方法对应的配置如下 constructoridArg columnid nameid/arg columntitle nametitle/arg columncontent namecontent/
/constructor 下面分析 constructor 节点的解析过程。如下 private void processConstructorElement(XNode resultChild, Class? resultType, ListResultMapping resultMappings) throws Exception {ListXNode argChildren resultChild.getChildren();for (XNode argChild : argChildren) {ListResultFlag flags new ArrayListResultFlag();flags.add(ResultFlag.CONSTRUCTOR);if (idArg.equals(argChild.getName())) {flags.add(ResultFlag.ID);}resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));}
} 如上上面方法的逻辑并不复杂。首先是获取并遍历子节点列表然后为每个子节点创建 flags 集合并添加 CONSTRUCTOR 标志。对于 idArg 节点额外添加 ID 标志。最后一步则是构建 ResultMapping该步逻辑前面已经分析过这里就不多说了。 分析完 的子节点 以及
2.1.3.3 ResultMap 对象构建过程分析
前面用了不少的篇幅来分析 子节点的解析过程。通过前面的分析我们可知 等节点最终都被解析成了 ResultMapping。在得到这些 ResultMapping 后紧接着要做的事情是构建 ResultMap。如果说 ResultMapping 与单条结果映射相对应那 ResultMap 与什么对应呢答案是…。答案暂时还不能说我们到源码中去找寻吧。下面让我们带着这个疑问开始本节的源码分析。
前面分析了很多源码大家可能都忘了 ResultMap 构建的入口了。这里再贴一下如下
private ResultMap resultMapElement(XNode resultMapNode, ListResultMapping 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,ListResultMapping 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);ListResultMapping extendedResultMappings new ArrayListResultMapping(resultMap.getResultMappings());extendedResultMappings.removeAll(resultMappings);boolean declaresConstructor false;for (ResultMapping resultMapping : resultMappings) {if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {declaresConstructor true;break;}}if (declaresConstructor) {IteratorResultMapping extendedResultMappingsIter extendedResultMappings.iterator();while (extendedResultMappingsIter.hasNext()) {if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {extendedResultMappingsIter.remove();}}}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 实例。过程如下 public ResultMap build() {if (resultMap.id null) {throw new IllegalArgumentException(ResultMaps must have an id);}resultMap.mappedColumns new HashSetString();resultMap.mappedProperties new HashSetString();resultMap.idResultMappings new ArrayListResultMapping();resultMap.constructorResultMappings new ArrayListResultMapping();resultMap.propertyResultMappings new ArrayListResultMapping();final ListString constructorArgNames new ArrayListString();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 ListString 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.);}Collections.sort(resultMap.constructorResultMappings, new ComparatorResultMapping() {Overridepublic int compare(ResultMapping o1, ResultMapping 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用于存储 、、、 节点 column 属性mappedProperties用于存储 和 节点的 property 属性或 和 节点的 name 属性idResultMappings用于存储 和 节点对应的 ResultMapping 对象propertyResultMappings用于存储 和 节点对应的 ResultMapping 对象constructorResultMappings用于存储 和 节点对应的 ResultMapping 对象
上面干巴巴的描述不够直观。下面我们写点代码测试一下并把这些集合的内容打印到控制台上大家直观感受一下。先定义一个映射文件如下
mapper namespacexyz.coolblog.dao.ArticleDaoresultMap idarticleResult typexyz.coolblog.model.ArticleconstructoridArg columnid nameid/arg columntitle nametitle/arg columncontent namecontent//constructorid propertyid columnid/result propertyauthor columnauthor/result propertycreateTime columncreate_time//resultMap
/mapper
测试代码如下
public class ResultMapTest {Testpublic void printResultMapInfo() throws Exception {Configuration configuration new Configuration();String resource mapper/ArticleMapper.xml;InputStream inputStream Resources.getResourceAsStream(resource);XMLMapperBuilder builder new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());builder.parse();ResultMap resultMap configuration.getResultMap(articleResult);System.out.println(\n-------------------✨ mappedColumns ✨--------------------);System.out.println(resultMap.getMappedColumns());System.out.println(\n------------------✨ mappedProperties ✨------------------);System.out.println(resultMap.getMappedProperties());System.out.println(\n------------------✨ idResultMappings ✨------------------);resultMap.getIdResultMappings().forEach(rm - System.out.println(simplify(rm)));System.out.println(\n---------------✨ propertyResultMappings ✨---------------);resultMap.getPropertyResultMappings().forEach(rm - System.out.println(simplify(rm)));System.out.println(\n-------------✨ constructorResultMappings ✨--------------);resultMap.getConstructorResultMappings().forEach(rm - System.out.println(simplify(rm)));System.out.println(\n-------------------✨ resultMappings ✨-------------------);resultMap.getResultMappings().forEach(rm - System.out.println(simplify(rm)));inputStream.close();}private String simplify(ResultMapping resultMapping) {return String.format(ResultMapping{column%s, property%s, flags%s, ...},resultMapping.getColumn(), resultMapping.getProperty(), resultMapping.getFlags());}
}
这里我们把5个集合转给你的内容都打印出来结果如下 如上结果比较清晰明了不需要过多解释了。我们参照上面配置文件及输出的结果把 ResultMap 的大致轮廓画出来。如下 到这里 节点的解析过程就分析完了。总的来说该节点的解析过程还是比较复杂的。好了其他的就不多说了继续后面的分析。
2.1.4 解析 节点 节点用来定义一些可重用的 SQL 语句片段比如表名或表的列名等。在映射文件中我们可以通过 节点引用 节点定义的内容。下面我来演示一下 节点的使用方式如下
sql idtablearticle
/sqlselect idfindOne resultTypeArticleSELECT id, title FROM include refidtable/ WHERE id #{id}
/selectupdate idupdate parameterTypeArticleUPDATE include refidtable/ SET title #{title} WHERE id #{id}
/update
如上上面配置中 和 节点通过 引入定义在 节点中的表名。上面的配置比较常规除了静态文本 节点还支持属性占位符 ${}。比如
sql idtable${table_prefix}_article
/sql
如果属性 table_prefix blog那么 节点中的内容最终为 blog_article。
上面介绍了 节点的用法比较容易。下面分析一下 sql 节点的解析过程如下
private void sqlElement(ListXNode list) throws Exception {if (configuration.getDatabaseId() ! null) {sqlElement(list, configuration.getDatabaseId());}sqlElement(list, null);
}
这个方法需要大家注意一下如果 Configuration 的 databaseId 不为空sqlElement 方法会被调用了两次。第一次传入具体的 databaseId用于解析带有 databaseId 属性且属性值与此相等的 节点。第二次传入的 databaseId 为空用于解析未配置 databaseId 属性的 节点。这里是个小细节大家注意一下就好。我们继续往下分析。
private void sqlElement(ListXNode list, String requiredDatabaseId) throws Exception {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);}}
}
这个方法逻辑比较简单首先是获取 节点的 id 和 databaseId 属性然后为 id 属性值拼接命名空间。最后通过检测当前 databaseId 和 requiredDatabaseId 是否一致来决定保存还是忽略当前的 节点。下面我们来看一下 databaseId 的匹配逻辑是怎样的。
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {if (requiredDatabaseId ! null) {if (!requiredDatabaseId.equals(databaseId)) {return false;}} else {if (databaseId ! null) {return false;}if (this.sqlFragments.containsKey(id)) {XNode context this.sqlFragments.get(id);if (context.getStringAttribute(databaseId) ! null) {return false;}}}return true;
}
下面总结一下 databaseId 的匹配规则。
databaseId 与 requiredDatabaseId 不一致即失配返回 false当前节点与之前的节点出现 id 重复的情况若之前的 节点 databaseId 属性不为空返回 false若以上两条规则均匹配失败此时返回 true
在上面三条匹配规则中第二条规则稍微难理解一点。这里简单分析一下考虑下面这种配置。 sql idtable databaseIdmysqlarticle
/sqlsql idtablearticle
/sql
在上面配置中两个 节点的 id 属性值相同databaseId 属性不一致。假设 configuration.databaseId mysql第一次调用 sqlElement 方法第一个 节点对应的 XNode 会被放入到 sqlFragments 中。第二次调用 sqlElement 方法时requiredDatabaseId 参数为空。由于 sqlFragments 中已包含了一个 id 节点且该节点的 databaseId 不为空此时匹配逻辑返回 false第二个节点不会被保存到 sqlFragments。
上面的分析内容涉及到了 databaseId关于 databaseId 的用途这里简单介绍一下。databaseId 用于标明数据库厂商的身份不同厂商有自己的 SQL 方言MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在 节点中有什么用呢这个问题也不难回答。 节点用于保存 SQL 语句片段如果 SQL 语句片段中包含方言的话那么该 节点只能被同一 databaseId 的查询语句或更新语句引用。关于 databaseId这里就介绍这么多。
好了本节内容先到这里。继续往下分析。
2.1.5 解析 SQL 语句节点
前面分析了 、、 以及 节点从这一节开始我们要分析映射文件中剩余的几个节点分别是 、、 以及 等。这几个节点中存储的是相同的内容都是 SQL 语句所以这几个节点的解析过程也是相同的。在进行代码分析之前这里需要特别说明一下为了避免和 节点混淆同时也为了描述方便这里把 、、 以及 等节点统称为 SQL 语句节点。好了下面开始本节的分析。
private void buildStatementFromContext(ListXNode list) {if (configuration.getDatabaseId() ! null) {buildStatementFromContext(list, configuration.getDatabaseId());}buildStatementFromContext(list, null);
}private void buildStatementFromContext(ListXNode 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);}}
}
上面的解析方法没有什么实质性的解析逻辑我们继续往下分析。
public void parseStatementNode() {String id context.getStringAttribute(id);String databaseId context.getStringAttribute(databaseId);if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}Integer fetchSize context.getIntAttribute(fetchSize);Integer timeout context.getIntAttribute(timeout);String parameterMap context.getStringAttribute(parameterMap);String parameterType context.getStringAttribute(parameterType);Class? parameterTypeClass resolveClass(parameterType);String resultMap context.getStringAttribute(resultMap);String resultType context.getStringAttribute(resultType);String lang context.getStringAttribute(lang);LanguageDriver langDriver getLanguageDriver(lang);Class? resultTypeClass resolveClass(resultType);String resultSetType context.getStringAttribute(resultSetType);StatementType statementType StatementType.valueOf(context.getStringAttribute(statementType, StatementType.PREPARED.toString()));ResultSetType resultSetTypeEnum resolveResultSetType(resultSetType);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());processSelectKeyNodes(id, parameterTypeClass, langDriver);SqlSource sqlSource langDriver.createSqlSource(configuration, context, parameterTypeClass);String resultSets context.getStringAttribute(resultSets);String keyProperty context.getStringAttribute(keyProperty);String keyColumn context.getStringAttribute(keyColumn);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;}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 实例
以上流程对应的代码比较复杂每个步骤都能分析出一些东西来。下面我会每个步骤都进行分析首先来分析 节点的解析过程。
2.1.5.1 解析 节点 节点的解析逻辑封装在 applyIncludes 中该方法的代码如下
public void applyIncludes(Node source) {Properties variablesContext new Properties();Properties configurationVariables configuration.getVariables();if (configurationVariables ! null) {variablesContext.putAll(configurationVariables);}applyIncludes(source, variablesContext, false);
}
上面代码创建了一个新的 Properties 对象并将全局 Properties 添加到其中。这样做的原因是 applyIncludes 的重载方法会向 Properties 中添加新的元素如果直接将全局 Properties 传给重载方法会造成全局 Properties 被污染。这是个小细节一般容易被忽视掉。其他没什么需要注意的了我们继续往下看。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {if (source.getNodeName().equals(include)) {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 !variablesContext.isEmpty()) {source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));}
}
上面的代码如果从上往下读不太容易看懂。因为上面的方法由三个条件分支外加两个递归调用组成代码的执行顺序并不是由上而下。要理解上面的代码我们需要定义一些配置并将配置带入到具体代码中逐行进行演绎。不过更推荐的方式是使用 IDE 进行单步调试。为了便于讲解我把上面代码中的三个分支都用 ⭐️ 标记了出来这个大家注意一下。好了必要的准备工作做好了下面开始演绎代码的执行过程。演绎所用的测试配置如下
mapper namespacexyz.coolblog.dao.ArticleDaosql idtable${table_name}/sqlselect idfindOne resultTypexyz.coolblog.dao.ArticleDOSELECTid, titleFROMinclude refidtableproperty nametable_name valuearticle//includeWHERE id #{id}/select
/mapper
我们先来看一下 applyIncludes 方法第一次被调用时的状态如下
参数值
source select 节点
节点类型ELEMENT_NODE
variablesContext [ ]
included false执行流程
1. 进入条件分支2
2. 获取 select 子节点列表
3. 遍历子节点列表将子节点作为参数进行递归调用
第一次调用 applyIncludes 方法source 代码进入条件分支2。在该分支中首先要获取 节点的子节点列表。可获取到的子节点如下
编号子节点类型描述1SELECT id, title FROMTEXT_NODE文本节点2ELEMENT_NODE普通节点3WHERE id #TEXT_NODE文本节点
在获取到子节点类列表后接下来要做的事情是遍历列表然后将子节点作为参数进行递归调用。在上面三个子节点中子节点1和子节点3都是文本节点调用过程一致。因此下面我只会演示子节点1和子节点2的递归调用过程。先来演示子节点1的调用过程如下 节点1的调用过程比较简单只有两层调用。然后我们在看一下子节点2的调用过程如下 上面是子节点2的调用过程共有四层调用略为复杂。大家自己也对着配置把源码走一遍然后记录每一次调用的一些状态这样才能更好的理解 applyIncludes 方法的逻辑。
好了本节内容先到这里继续往下分析。
2.1.5.2 解析 节点
对于一些不支持自增主键的数据库来说我们在插入数据时需要明确指定主键数据。以 Oracle 数据库为例Oracle 数据库不支持自增主键但它提供了自增序列工具。我们每次向数据库中插入数据时可以先通过自增序列获取主键数据然后再进行插入。这里涉及到两次数据库查询操作我们不能在一个 节点中同时定义两个 select 语句否者会导致 SQL 语句出错。对于这个问题MyBatis 的 可以很好的解决。下面我们看一段配置
insert idsaveAuthorselectKey keyPropertyid resultTypeint orderBEFOREselect author_seq.nextval from dual/selectKeyinsert into Author(id, name, password)values(#{id}, #{username}, #{password})
/insert
在上面的配置中查询语句会先于插入语句执行这样我们就可以在插入时获取到主键的值。关于 的用法这里不过多介绍了。下面我们来看一下 节点的解析过程。
private void processSelectKeyNodes(String id, Class? parameterTypeClass, LanguageDriver langDriver) {ListXNode selectKeyNodes context.evalNodes(selectKey);if (configuration.getDatabaseId() ! null) {parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());}parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);removeSelectKeyNodes(selectKeyNodes);
}
从上面的代码中可以看出 节点在解析完成后会被从 dom 树中移除。这样后续可以更专注的解析 或 节点中的 SQL无需再额外处理 节点。继续往下看。
private void parseSelectKeyNodes(String parentId, ListXNode 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 实例
在这三步中第1步和第2步调用的是公共逻辑其他地方也会调用这两步对应的源码后续会分两节进行讲解。第3步则是创建一个 SelectKeyGenerator 实例SelectKeyGenerator 创建的过程本身没什么好说的所以就不多说了。下面分析一下 SqlSource 和 MappedStatement 实例的创建过程。
2.1.5.3 解析 SQL 语句
前面分析了 和 节点的解析过程这两个节点解析完成后都会以不同的方式从 dom 树中消失。所以目前的 SQL 语句节点由一些文本节点和普通节点组成比如 、 等。那下面我们来看一下移除掉 和 节点后的 SQL 语句节点是如何解析的。 public SqlSource createSqlSource(Configuration configuration, XNode script, Class? parameterType) {XMLScriptBuilder builder new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();
}public SqlSource parseScriptNode() {MixedSqlNode rootSqlNode parseDynamicTags(context);SqlSource sqlSource null;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。下面我们来看一下 parseDynamicTags 方法的逻辑。 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());
}protected MixedSqlNode parseDynamicTags(XNode node) {ListSqlNode contents new ArrayListSqlNode();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 实现类还有一些 SqlNode 实现类未出现在上面的代码中。但它们也参与了 SQL 语句节点的解析过程这里我们来看一下这些幕后的 SqlNode 类。 上面的 SqlNode 实现类用于处理不同的动态 SQL 逻辑这些 SqlNode 是如何生成的呢答案是由各种 NodeHandler 生成。我们再回到上面的代码中可以看到这样一句代码
handler.handleNode(child, contents);
该代码用于处理动态 SQL 节点并生成相应的 SqlNode。下面来简单分析一下 WhereHandler 的代码。 private class WhereHandler implements NodeHandler {public WhereHandler() {}Overridepublic void handleNode(XNode nodeToHandle, ListSqlNode 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。下面我们来看一下它的构建过程。
2.1.5.4 构建 MappedStatement
SQL 语句节点可以定义很多属性这些属性和属性值最终存储在 MappedStatement 中。下面我们看一下 MappedStatement 的构建过程是怎样的。
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)).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).resultSetType(resultSetType).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没什么复杂的地方就不多说了。
2.1.6 小节
本章分析了映射文件的解析过程总的来说本章的内容还是比较复杂的逻辑太多。不过如果大家自己也能把映射文件的解析过程认真分析一遍会对 MyBatis 有更深入的理解。分析过程很累但是在此过程中会收获了很多东西还是很开心的。好了本章内容先到这里。后面还有一些代码需要分析我们继续往后看。
2.2 Mapper 接口绑定过程分析
映射文件解析完成后并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定 mapper 接口这样才能将映射文件中的 SQL 语句和 mapper 接口中的方法绑定在一起后续即可通过调用 mapper 接口方法执行与之对应的 SQL 语句。下面我们来分析一下 mapper 接口的绑定过程。 private void bindMapperForNamespace() {String namespace builderAssistant.getCurrentNamespace();if (namespace ! null) {Class? boundType null;try {boundType Resources.classForName(namespace);} catch (ClassNotFoundException e) {}if (boundType ! null) {if (!configuration.hasMapper(boundType)) {configuration.addLoadedResource(namespace: namespace);configuration.addMapper(boundType);}}}
}public T void addMapper(ClassT type) {mapperRegistry.addMapper(type);
}public T void addMapper(ClassT 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 MapperProxyFactoryT(type));MapperAnnotationBuilder parser new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}
}
以上就是 Mapper 接口的绑定过程。这里简单一下
获取命名空间并根据命名空间解析 mapper 类型将 type 和 MapperProxyFactory 实例存入 knownMappers 中解析注解中的信息
以上步骤中第3步的逻辑较多。如果大家看懂了映射文件的解析过程那么注解的解析过程也就不难理解了这里就不深入分析了。好了Mapper 接口的绑定过程就先分析到这。
2.3 处理未完成解析的节点
在解析某些节点的过程中如果这些节点引用了其他一些未被解析的配置会导致当前节点解析工作无法进行下去。对于这种情况MyBatis 的做法是抛出 IncompleteElementException 异常。外部逻辑会捕捉这个异常并将节点对应的解析器放入 incomplet* 集合中。这个我在分析映射文件解析的过程中进行过相应注释不知道大家有没有注意到。没注意到也没关系待会我会举例说明。下面我们来看一下 MyBatis 是如何处理未完成解析的节点。 public void parse() {configurationElement(parser.evalNode(/mapper));parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();
}
如上parse 方法是映射文件的解析入口。在本章的开始我贴过这个源码。从上面的源码中可以知道有三种节点在解析过程中可能会出现不能完成解析的情况。由于上面三个以 parsePending 开头的方法逻辑一致所以下面我只会分析其中一个方法的源码。简单起见这里选择分析 parsePendingCacheRefs 的源码。下面看一下如何配置映射文件会导致 节点无法完成解析。 mapper namespacexyz.coolblog.dao.Mapper1cache-ref namespacexyz.coolblog.dao.Mapper2/
/mappermapper namespacexyz.coolblog.dao.Mapper2cache/
/mapper
如上假设 MyBatis 先解析映射文件1然后再解析映射文件2。按照这样的解析顺序映射文件1中的 节点就无法完成解析因为它所引用的缓存还未被解析。当映射文件2解析完成后MyBatis 会调用 parsePendingCacheRefs 方法处理在此之前未完成解析的 节点。具体的逻辑如下
private void parsePendingCacheRefs() {CollectionCacheRefResolver incompleteCacheRefs configuration.getIncompleteCacheRefs();synchronized (incompleteCacheRefs) {IteratorCacheRefResolver iter incompleteCacheRefs.iterator();while (iter.hasNext()) {try {iter.next().resolveCacheRef();iter.remove();} catch (IncompleteElementException e) {}}}
}
上面代码不是很长我也做了比较多的注释应该不难理解。好了关于未完成解析节点的解析过程就分析到这。
3.总结
本篇文章对映射文件的解析过程进行了较为详细的分析全文篇幅比较大写的也比较辛苦。本篇文章耗时7天完成在这7天中基本上一有空闲时间就会用来写作。虽然很累但是收获也很多。我目前正在努力的构建自己的知识体系我觉得对于常用的技术还是应该花一些时间和精力去弄懂它的原理。这样以后才能走的更远才能成为你想成为的样子。
好了其他的就不多说了本篇文章就到这吧。谢谢大家的阅读。
参考
《MyBatis 技术内幕》- 徐郡明MyBatis 官方文档 原文地址https://www.cnblogs.com/nullllun/p/9388667.html