上一篇我们分析了配置文件的加载与解析过程,本文将继续对映射文件的加载与解析实现进行分析。MyBatis 的映射文件用于配置 SQL 语句、二级缓存,以及结果集映射等,是区别于其它 ORM 框架的主要特色之一。

在上一篇分析配置文件 <mappers/> 标签的解析实现时,了解到 MyBatis 最终通过调用 XMLMapperBuilder#parse 方法实现对映射文件的解析操作,本文我们将以此方法作为入口,探究 MyBatis 加载和解析映射文件的实现机制。

方法 XMLMapperBuilder#parse 的实现如下:

public void parse() {/* 1. 加载并解析映射文件 */if (!configuration.isResourceLoaded(resource)) {// 加载并解析 <mapper/> 标签下的配置this.configurationElement(parser.evalNode("/mapper"));// 标记该映射文件已被解析configuration.addLoadedResource(resource);// 注册当前映射文件关联的 Mapper 接口(标签 <mapper namespace=""/> 对应的 namespace 属性)this.bindMapperForNamespace();}/* 2. 处理解析过程中失败的标签 */// 处理解析失败的 <resultMap/> 标签this.parsePendingResultMaps();// 处理解析失败的 <cache-ref/> 标签this.parsePendingCacheRefs();// 处理解析失败的 SQL 语句标签this.parsePendingStatements();
}

MyBatis 在解析映射文件时首先会判断该映射文件是否被解析过,对于没有被解析过的文件则会调用 XMLMapperBuilder#configurationElement 方法解析所有配置,并注册当前映射文件关联的 Mapper 接口。对于解析过程中处理异常的标签,MyBatis 会将其记录到 Configuration 对象对应的属性中,并在方法最后再次尝试二次解析。

整个 XMLMapperBuilder#configurationElement 方法实现了对映射文件解析的核心步骤,与配置文件解析的实现方式一样,这也是一个调度方法,实现如下:

private void configurationElement(XNode context) {try {// 获取 <mapper/> 标签的 namespace 属性,设置当前映射文件关联的 Mapper 接口String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 解析 <cache-ref/> 子标签,多个 mapper 可以共享同一个二级缓存this.cacheRefElement(context.evalNode("cache-ref"));// 解析 <cache/> 子标签this.cacheElement(context.evalNode("cache"));// 解析 <parameterMap/> 子标签,已废弃this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析 <resultMap/> 子标签,建立结果集与对象属性之间的映射关系this.resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析 <sql/> 子标签this.sqlElement(context.evalNodes("/mapper/sql"));// 解析 <select/>、<insert/>、<update/> 和 <delete/> 子标签this.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);}
}

每个映射文件都关联一个具体的 Mapper 接口,而 <mapper/> 节点的 namespace 属性则用于指定对应的 Mapper 接口限定名。上述方法首先会获取 namespace 属性,然后调用相应方法对每个子标签进行解析,下面逐一展开分析。

加载与解析映射文件

下面对各个子标签的解析过程逐一展开分析,考虑到 <parameterMap/> 子标签已废弃,所以不再对其多作介绍。

解析 cache 标签

MyBatis 在设计上分为一级缓存和二级缓存(关于缓存机制将会在下一篇分析 SQL 语句执行过程时进行介绍,这里只要知道有这样两个概念即可),该标签用于对二级缓存进行配置。在具体分析 <cache/> 标签之前,我们需要对 MyBatis 的缓存类设计有一个了解,不然可能会云里雾里。MyBatis 的缓存类设计还是非常巧妙的,不管是一级缓存还是二级缓存,都实现自同一个 Cache 接口:

public interface Cache {/** 缓存对象 ID */String getId();/** 添加数据到缓存,一般来说 key 是 {@link CacheKey} 类型 */void putObject(Object key, Object value);/** 从缓存中获取 key 对应的 value */Object getObject(Object key);/** 从缓存中移除指定对象 */Object removeObject(Object key);/** 清空缓存 */void clear();/*** 获取缓存对象的个数(不是缓存的容量),* 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现*/int getSize();/*** 缓存读写锁,* 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现*/ReadWriteLock getReadWriteLock();
}

Cache 接口中声明的缓存操作方法中规中矩。围绕该接口,MyBatis 实现了基于 HashMap 数据结构的基本实现 PerpetualCache 类,该实现类的各项方法实现都是对 HashMap API 的封装,比较简单。在整个缓存类设计方面,MyBatis 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。

介绍完了缓存类的基本设计,我们再回过头来继续分析<cache/>标签的解析过程,由XMLMapperBuilder#cacheElement方法实现:

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"); // 缓存清除策略,默认是 LRU,还可以是 FIFO、SOFT,以及 WEAKClass<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);Long flushInterval = context.getLongAttribute("flushInterval"); // 刷新间隔,单位:毫秒Integer size = context.getIntAttribute("size"); // 缓存大小,默认为 1024boolean readWrite = !context.getBooleanAttribute("readOnly", false); // 是否只读boolean blocking = context.getBooleanAttribute("blocking", false); // 是否阻塞Properties props = context.getChildrenAsProperties();// 创建二级缓存,并填充 Configuration 对象builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}
}

上述方法首先会获取<cache/>标签的相关属性配置,然后调用MapperBuilderAssistant#useNewCache方法创建缓存对象,并记录到 Configuration 对象中。方法MapperBuilderAssistant#useNewCache中使用了缓存对象构造器 CacheBuilder 创建缓存对象,一起来看一下CacheBuilder#build方法实现:

public Cache build() {// 如果没有指定自定义缓存实现类,则设置缓存默认实现(以 PerpetualCache 作为默认实现,以 LruCache 作为默认装饰器)this.setDefaultImplementations();// 反射创建缓存对象Cache cache = this.newBaseCacheInstance(implementation, id);// 初始化缓存对象this.setCacheProperties(cache);// issue #352, do not apply decorators to custom caches// 如果缓存采用 PerpetualCache 实现,则遍历使用注册的装饰器进行装饰if (PerpetualCache.class.equals(cache.getClass())) {// 遍历装饰器集合,基于反射方式装饰缓存对象for (Class<? extends Cache> decorator : decorators) {cache = this.newCacheDecoratorInstance(decorator, cache);this.setCacheProperties(cache);}// 采用标准装饰器进行装饰cache = this.setStandardDecorators(cache);}// 采用日志缓存装饰器对缓存对象进行装饰else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {cache = new LoggingCache(cache);}return cache;
}

构造缓存对象时首先会判断是否指定了自定义的缓存实现类,否则使用默认的缓存实现(即以 PerpetualCache 作为默认实现,以 LruCache 作为默认缓存装饰器);然后选择 String 类型参数的构造方法构造缓存对象,并基于配置对缓存对象进行初始化;最后依据缓存实现采用相应的装饰器予以装饰。

方法 CacheBuilder#setCacheProperties 除了用于设置相应属性配置外,还会判断缓存类是否实现了 InitializingObject 接口,以决定是否调用 InitializingObject#initialize 初始化方法。

解析 cache-ref 标签

标签 <cache/> 默认的作用域限定在标签所在的 namespace 范围内,如果希望能够让一个缓存对象在多个 namespace 之间共享,可以定义 <cache-ref/> 标签以引用其它命名空间中定义的缓存对象。标签 <cache-ref/> 的解析位于 XMLMapperBuilder#cacheRefElement 方法中:

private void cacheRefElement(XNode context) {if (context != null) {// 记录 <当前节点所在的 namespace, 引用缓存对象所在的 namespace> 映射关系到 Configuration 中configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));// 构造缓存引用解析器 CacheRefResolver 对象CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));try {// 从记录缓存对象的 Configuration#caches 集合中获取引用的缓存对象cacheRefResolver.resolveCacheRef();} catch (IncompleteElementException e) {// 如果解析出现异常则记录到 Configuration#incompleteCacheRefs 中,稍后再处理configuration.addIncompleteCacheRef(cacheRefResolver);}}
}

方法首先会在 Configuration#cacheRefMap 属性中记录一下当前的引用关系,其中 key 是 <cache-ref/> 所在的 namespace,value 则是引用的缓存对象所在的 namespace。然后从 Configuration#caches 属性中获取引用的缓存对象,在分析 <cache/> 标签时,我们曾提及到最终解析构造的缓存对象会记录到 Configuration#caches 属性中,这里则是一个逆过程。

解析 resultMap 标签

标签 <resultMap/> 用于配置结果集映射,建立结果集与实体类对象属性之间的映射关系。这是一个非常有用且提升开发效率的配置,如果是纯 JDBC 开发,在处理结果集与实体类对象之间的映射时还需要手动硬编码注入。对于一张字段较多的表来说,简直写到手抽筋,而 <resultMap/> 标签配置配合 mybatis-generator 工具的逆向工程可以解放我们的双手。下面是一个典型的配置,用于建立数据表 t_user 与 User 实体类之间的属性映射关系:

<resultMap id="BaseResultMap" type="org.zhenchao.mybatis.entity.User"><id column="id" jdbcType="BIGINT" property="id"/><result column="username" jdbcType="VARCHAR" property="username"/><result column="password" jdbcType="VARCHAR" property="password"/><result column="age" jdbcType="INTEGER" property="age"/><result column="phone" jdbcType="VARCHAR" property="phone"/><result column="email" jdbcType="VARCHAR" property="email"/>
</resultMap>

在开始介绍 <resultMap/> 标签的解析过程之前,我们需要对该标签涉及到的两个主要的类 ResultMapping 和 ResultMap 有一个了解。前者用于封装除 <discriminator/> 标签以外的其它子标签配置(该标签具备自己的封装类),后者则用于封装整个 <resultMap/> 标签。

  • ResultMapping
public class ResultMapping {private Configuration configuration;/** 对应标签的 property 属性 */private String property;/** 对应标签的 column 属,配置数据表列名(or 别名) */private String column;/** 对应 java 类型,配置类型全限定名(or 别名) */private Class<?> javaType;/** 对应列的 JDBC 类型 */private JdbcType jdbcType;/** 类型处理器,会覆盖默认类型处理器 */private TypeHandler<?> typeHandler;/** 对应标签的 resultMap 属性,以 id 的方式引某个已定义的 <resultMap/> */private String nestedResultMapId;/** 对应标签的 select 属性,以 id 的方式引用某个已定义的 <select/> */private String nestedQueryId;/** 对标签的 notNullColumns 属性 */private Set<String> notNullColumns;/** 对应标签的 columnPrefix 属性 */private String columnPrefix;/** 记录处理后的标志 */private List<ResultFlag> flags;/** 记录标签 column 拆分后生成的结果 */private List<ResultMapping> composites;/** 对应标签 resultSet 属性 */private String resultSet;/** 对应标签 foreignColumn 属性 */private String foreignColumn;/** 对应标签 fetchType 属性,配置是否延迟加载 */private boolean lazy;// ... 省略构造器类定义,以及 getter 和 setter 方法
}

ResultMapping 类中定义的属性如上述代码注释。此外,还内置了一个 Builder 内部构造器类,用于封装数据构造 ResultMapping 对象,并实现了对属性值的基本校验逻辑。

  • ResultMap
public class ResultMap {private Configuration configuration;/** 对应标签的 id 属性 */private String id;/** 对应标签的 type 属性 */private Class<?> type;/** 记录除 <discriminator/> 标签以外的其它映射关系 */private List<ResultMapping> resultMappings;/** 记录带有 id 属性的映射关系 */private List<ResultMapping> idResultMappings;/** 记录带有 constructor 属性的映射关系 */private List<ResultMapping> constructorResultMappings;/** 记录带有 property 属性的映射关系 */private List<ResultMapping> propertyResultMappings;/** 记录配置中所有的 column 属性集合 */private Set<String> mappedColumns;/** 记录配置中所有的 property 属性集合 */private Set<String> mappedProperties;/** 封装 <discriminator/> 标签 */private Discriminator discriminator;/** 是否包含嵌套的结果映射 */private boolean hasNestedResultMaps;/** 是否包含嵌套查询 */private boolean hasNestedQueries;/** 是否开启自动映射 */private Boolean autoMapping;// ... 省略构造器类,以及 getter 和 setter 方法
}

ResultMap 类中定义的属性如上述代码注释。与 ResultMapping 一样,也是通过内置 Builder 内部构造器类来构造 ResultMap 对象,构造器的实现比较简单,读者可以参考源码实现。

了解了内部数据结构 ResultMapping 和 ResultMap 的定义,以及二者之间的相互依赖关系,接下来开始分析 <resultMap/> 标签的解析过程,实现位于 XMLMapperBuilder#resultMapElements 方法中:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());// 获取 type 属性,支持 type、ofType、resultType,以及 javaType 类型配置String type = resultMapNode.getStringAttribute("type",resultMapNode.getStringAttribute("ofType",resultMapNode.getStringAttribute("resultType",resultMapNode.getStringAttribute("javaType"))));// 基于 TypeAliasRegistry 解析 type 属性对应的实体类型Class<?> typeClass = this.resolveClass(type);if (typeClass == null) {// 尝试基于 <association/> 子标签或 <case/> 子标签解析实体类型typeClass = this.inheritEnclosingType(resultMapNode, enclosingType);}Discriminator discriminator = null;// 用于记录解析结果List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);// 获取并遍历处理所有的子标签List<XNode> resultChildren = resultMapNode.getChildren();for (XNode resultChild : resultChildren) {// 解析 <constructor/> 子标签,封装成为 ResultMapping 对象if ("constructor".equals(resultChild.getName())) {this.processConstructorElement(resultChild, typeClass, resultMappings);}// 解析 <discriminator/> 子标签,封装成为 Discriminator 对象else if ("discriminator".equals(resultChild.getName())) {discriminator = this.processDiscriminatorElement(resultChild, typeClass, resultMappings);}// 解析 <association/>、<collection/>、<id/> 和 <result/> 子标签,封装成为 ResultMapping 对象else {List<ResultFlag> flags = new ArrayList<>();if ("id".equals(resultChild.getName())) {flags.add(ResultFlag.ID);}// 创建 ResultMapping 对象,并记录到 resultMappings 集合中resultMappings.add(this.buildResultMappingFromContext(resultChild, typeClass, flags));}}// 获取 id 属性(标识当前 <resultMap/> 标签),如果没有指定则基于规则生成一个String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());// 获取 extends 属性,用于指定继承关系String extend = resultMapNode.getStringAttribute("extends");// 获取 autoMapping 属性,是否启用自动映射(自动查找与列名相同的属性名称,并执行注入)Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);try {// 基于解析得到的配置构造 ResultMap 对象,记录到 Configuration#resultMaps 中return resultMapResolver.resolve();} catch (IncompleteElementException e) {// 记录解析异常的 <resultMap/> 标签,后续尝试二次解析configuration.addIncompleteResultMap(resultMapResolver);throw e;}
}

标签 <resultMap/> 包含 4 个属性配置,即 id、type、extends 和 autoMapping。

  • id:标识当前 <resultMap/> 标签,如果没有指定则会调用 XNode#getValueBasedIdentifier 方法基于规则自动生成一个,用于提升 MyBatis 的执行性能。
  • type:设置当前标签所关联的实体类对象,支持 type、ofType、resultType,以及 javaType 等配置方式,以尽可能用简单的配置支持更多的实体类型。
  • extends:指定当前标签的继承关系。
  • autoMapping:一个 boolean 类型的配置项,如果为 true 则表示开启自动映射功能,MyBatis 会自动查找实例类对象中与结果集列名相同的属性名,并调用 setter 方法执行注入。标签 <resultMap/> 中明确指定的映射关系优先级要高于自动映射。

标签 <resultMap/> 包含 <constructor/><id/><result/><association/><collection/>,以及 <discriminator/> 六个子标签。关于这些子标签的作用可以参阅 官方文档,除 <discriminator/> 以外,其余五个标签的解析实现大同小异,下面以 <constructor/> 标签为例对解析实现展开分析。

子标签 <constructor/> 的解析由 XMLMapperBuilder#processConstructorElement 方法实现,如下:

private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {// 获取并处理 <constructor/> 标签中配置的子标签列表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); // 添加 ID 标识}// 封装标签配置为 ResultMapping 对象,记录到 resultMappings 集合中resultMappings.add(this.buildResultMappingFromContext(argChild, resultType, flags));}
}

子标签<constructor/>用于指定实体类的构造方法以实现在构造实体类对象时注入结果值。上述方法直接遍历处理该标签的所有子标签,即<idArg/><arg/>,并调用XMLMapperBuilder#buildResultMappingFromContext方法创建对应的 ResultMapping 对象,实现如下:

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", () ->this.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"));// 基于 TypeAliasRegistry 解析 JavaType 对应的 Class 对象Class<?> javaTypeClass = this.resolveClass(javaType);// 基于 TypeAliasRegistry 解析 TypeHandler 对应的 Class 对象Class<? extends TypeHandler<?>> typeHandlerClass = this.resolveClass(typeHandler);// 获取 JdbcType 对应的具体枚举对象JdbcType jdbcTypeEnum = this.resolveJdbcType(jdbcType);// 封装成 ResultMapping 对象return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap,notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

方法首先会获取标签所有的属性配置项,并基于 TypeAliasRegistry 对属性所表示的类型进行解析,最后调用 MapperBuilderAssistant#buildResultMapping 方法构造封装配置项对应的 ResultMapping 对象,这里本质上还是调用 ResultMapping 的构造器进行构造。其中,方法 XMLMapperBuilder#buildResultMappingFromContext 是一个通用方法,除了上面用于封装 <constructor/> 子标签,对于标签 <id/><result/><association/><collection/> 来说也都是直接调用该方法进行解析。

继续来看一下 <discriminator/> 标签,该标签并没有直接采用 ResultMapping 类进行封装,而是采用 Discriminator 类对 ResultMapping 进行封装,这主要取决于该标签的用途。MyBatis 使用该标签基于具体的结果值选择不同的结果集映射,解析实现位于 XMLMapperBuilder#processDiscriminatorElement 方法中,如下:

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {// 获取相关属性配置String column = context.getStringAttribute("column");String javaType = context.getStringAttribute("javaType");String jdbcType = context.getStringAttribute("jdbcType");String typeHandler = context.getStringAttribute("typeHandler");// 基于 TypeAliasRegistry 解析类型属性对应的 Class 对象Class<?> javaTypeClass = this.resolveClass(javaType);Class<? extends TypeHandler<?>> typeHandlerClass = this.resolveClass(typeHandler);JdbcType jdbcTypeEnum = this.resolveJdbcType(jdbcType);// 遍历处理子标签列表Map<String, String> discriminatorMap = new HashMap<>();for (XNode caseChild : context.getChildren()) {String value = caseChild.getStringAttribute("value");String resultMap = caseChild.getStringAttribute("resultMap",// 嵌套解析this.processNestedResultMappings(caseChild, resultMappings, resultType));discriminatorMap.put(value, resultMap);}// 封装成 Discriminator 对象,本质上依赖于 Discriminator 的构造器构建return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

可以看到,具体的解析步骤与其它标签如出一辙,参考代码注释。

在将这六类子标签解析成为相应对象并记录到 resultMappings 集合中之后,下一步就是基于这些配置构造 ResultMapResolver 解析器,并调用 ResultMapResolver#resolve 方法解析 <resultMap/> 配置为 ResultMap 对象记录到 Configuration#resultMaps 属性中。

方法 ResultMapResolver#resolve 直接将请求委托给了 MapperBuilderAssistant#addResultMap 方法执行,实现如下:

public ResultMap resolve() {return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}// org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap
public ResultMap addResultMap(String id,Class<?> type,String extend,Discriminator discriminator,List<ResultMapping> resultMappings,Boolean autoMapping) {// 格式化 id 值,格式:namespace.idid = this.applyCurrentNamespace(id, false);extend = this.applyCurrentNamespace(extend, true);// 处理 extend 配置if (extend != null) {// 被继承的 ResultMap 不存在if (!configuration.hasResultMap(extend)) {throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");}// 获取需要被继承的 ResultMap 对象ResultMap resultMap = configuration.getResultMap(extend);// 获取父 ResultMap 对象中包含的 ResultMapping 对象集合List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());// 删除被覆盖的 ResultMapping 对象extendedResultMappings.removeAll(resultMappings);// Remove parent constructor if this resultMap declares a constructor.boolean declaresConstructor = false;// 查找当前 <resultMap/> 标签中是否定义了 <constructor/> 子标签for (ResultMapping resultMapping : resultMappings) {if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {declaresConstructor = true;break;}}// 当前 <resultMap/> 中定义了 <constructor/> 子标签,// 则无需父 ResultMap 中记录的相应 <constructor/>,遍历删除if (declaresConstructor) {extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));}// 添加需要继承的 ResultMapping 对象集合resultMappings.addAll(extendedResultMappings);}// 创建 ResultMap 对象,并记录到 Configuration#resultMaps 中ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build();configuration.addResultMap(resultMap);return resultMap;
}

具体过程如代码注释。

解析 sql 标签

在 MyBatis 中可以通过 <sql/> 标签配置一些可以被复用的 SQL 语句片段,当我们在某个 SQL 语句中需要使用这些片段时,可以通过 <include/> 子标签引入,具体示例可以参考 官方文档。对于 <sql/> 标签的解析由 XMLMapperBuilder#sqlElement 方法实现,最终记录到 Configuration#sqlFragments 集合中,方法实现如下:

private void sqlElement(List<XNode> list, String requiredDatabaseId) {// 遍历处理所有的 <sql/> 标签for (XNode context : list) {// 获取数据库标识 databaseId 属性String databaseId = context.getStringAttribute("databaseId");// 获取 id 属性String id = context.getStringAttribute("id");// 格式化 id,格式:namespace.idid = builderAssistant.applyCurrentNamespace(id, false);/** 判断数据库标识 databaseId 与当前 Configuration 中配置的是否一致:* 1. 如果指定了 requiredDatabaseId,则 databaseId 必须和 requiredDatabaseId 一致* 2. 如果没有指定了 requiredDatabaseId,则 databaseId 必须为 null*/if (this.databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {sqlFragments.put(id, context);}}
}

方法首先会获取<sql/>标签的属性配置,即 id 和 databaseId,并对 id 进行格式化处理;然后判断当前<sql/>标签配置的数据库标识 databaseId 是否与当前运行的数据库环境相匹配,并忽略不匹配的<sql/>标签。参数 requiredDatabaseId 在重载方法中指定,本质上就是从全局配置 Configuration 对象中获取的Configuration#databaseId属性值:

private void sqlElement(List<XNode> list) {if (configuration.getDatabaseId() != null) {// 获取当前运行环境对应的数据库标识this.sqlElement(list, configuration.getDatabaseId());}this.sqlElement(list, null);
}

最终这些解析得到的 <sql/> 标签会被记录到 Configuration#sqlFragments 属性中(在构造 XMLMapperBuilder 对象时进行初始化),后面分析 <include/> 标签时可以看到会从该属性值获取引用的 SQL 语句片段。

解析 select / insert / update / delete 标签

标签 <select/><insert/><update/><delete/> 用于配置映射文件中最核心的数据库操作语句(下文统称这 4 个标签为 SQL 语句标签),包括静态 SQL 语句和动态 SQL 语句。MyBatis 通过 MappedStatement 类封装这些 SQL 语句标签的配置,并调用 XMLStatementBuilder#parseStatementNode 方法对标签进行解析,构建 MappedStatement 对象并记录到 Configuration#mappedStatements 属性中。

方法 XMLMapperBuilder#buildStatementFromContext 对于标签的解析主要做了一些统筹调度的工作,具体解析还是交由 XMLStatementBuilder 类进行处理,该方法的实现如下:

private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) {this.buildStatementFromContext(list, configuration.getDatabaseId());}this.buildStatementFromContext(list, null);
}private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {// 遍历处理获取到的所有 SQL 语句标签for (XNode context : list) {// 创建 XMLStatementBuilder 解析器,负责解析具体的 SQL 语句标签final XMLStatementBuilder statementParser =new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {// 执行解析操作statementParser.parseStatementNode();} catch (IncompleteElementException e) {// 记录解析异常的 SQL 语句标签,稍后尝试二次解析configuration.addIncompleteStatement(statementParser);}}
}

上述实现比较简单,无非是遍历获取到的所有 SQL 语句标签列表,然后构建 XMLStatementBuilder 解析器并调用 XMLStatementBuilder#parseStatementNode 方法对各个 SQL 语句标签进解析。对于解析异常的标签则会记录到 Configuration#incompleteStatements 属性中,稍后会再次尝试解析。

下面分析一下 XMLStatementBuilder#parseStatementNode 方法的具体实现:

public void parseStatementNode() {// 获取 id 和 databaseId 属性String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");// 判断当前 SQL 语句是否适配当前数据库类型,忽略不适配的 SQL 语句if (!this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}/* 获取并解析属性配置 */// 解析 SQL 语句类型String nodeName = context.getNode().getNodeName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 标识是否是 SELECT 语句boolean isSelect = sqlCommandType == SqlCommandType.SELECT;// 标识任何时候只要语句被调用,都会导致本地缓存和二级缓存被清空,适用于修改数据操作boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);// 设置本条语句的结果是否被二级缓存,默认适用于 SELECT 语句boolean useCache = context.getBooleanAttribute("useCache", isSelect);// 仅针对嵌套结果 SELECT 语句适用boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// 解析 <include/> 子标签XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 解析传入参数类型的完全限定名或别名String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = this.resolveClass(parameterType);String lang = context.getStringAttribute("lang");LanguageDriver langDriver = this.getLanguageDriver(lang);// 解析 <selectKey/> 子标签this.processSelectKeyNodes(id, parameterTypeClass, langDriver);// 解析对应的 KeyGenerator 实现,用于生成填充 keyProperty 属性指定的列值KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);// 当前 SQL 语句标签下存在 <selectKey/> 配置,直接获取对应的 SelectKeyGeneratorif (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);}// 当前 SQL 语句标签下不存在 <selectKey/> 配置else {// 依据当前标签的 useGeneratedKeys 配置,或全局的 useGeneratedKeys 配置,以及是否是 INSERT 方法来决定具体的 keyGenerator 实现// 属性 useGeneratedKeys 仅对 INSERT 和 UPDATE 有用,使用 JDBC 的 getGeneratedKeys 方法取出由数据库内部生成的主键keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 创建 SQL 语句标签对应的 SqlSource 对象SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// 获取具体的 Statement 类型,默认使用 PreparedStatementStatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 设置批量返回的结果行数,默认值为 unset(依赖驱动)Integer fetchSize = context.getIntAttribute("fetchSize");// 数据库执行超时时间(单位:秒),默认值为 unset(依赖驱动)Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap"); // 已废弃// 期望返回类型完全限定名或别名,对于集合类型应该是集合元素类型,而非集合类型本身String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = this.resolveClass(resultType);// 引用的 <resultMap/> 的标签 IDString resultMap = context.getStringAttribute("resultMap");// FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}// (仅对 INSERT 和 UPDATE 有用)唯一标记一个属性,通过 getGeneratedKeys 的返回值或者通过 INSERT 语句的 selectKey 子标签设置它的键值String keyProperty = context.getStringAttribute("keyProperty");// (仅对 INSERT 和 UPDATE 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(如 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置String keyColumn = context.getStringAttribute("keyColumn");// 仅对多结果集适用,将列出语句执行后返回的结果集并给每个结果集一个名称,名称采用逗号分隔String resultSets = context.getStringAttribute("resultSets");// 创建当前 SQL 语句配置对应的 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 语句标签的过程如上述代码注释,配合官方文档对于各个属性和子标签作用的解释应该不难理解,关于子标签 <include/><selectKey/> 的解析实现稍后会详细说明。

MyBatis 使用 MappedStatement 对象封装 SQL 语句标签配置,并记录到 Configuration#mappedStatements 属性中,在这个过程中会调用 LanguageDriver#createSqlSource 方法创建 SQL 语句标签对应的 SqlSource 对象。SqlSource 类用于封装 SQL 语句标签(或 Mapper 接口方法注解)中配置的 SQL 语句,但是这里的 SQL 语句暂时还不能被数据库执行,因为其中可能包含占位符。关于 SqlSource 类暂时先了解其作用即可,稍后会对其实现做详细介绍,下面先来看一下 LanguageDriver#createSqlSource 方法的实现,具体实现类为 XMLLanguageDriver:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();
}// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() {// 判断是否是动态 SQL,解析封装为 MixedSqlNode 对象MixedSqlNode rootSqlNode = this.parseDynamicTags(context);SqlSource sqlSource;// 动态 SQL,封装为 DynamicSqlSource 对象if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);}// 静态 SQL,封装为 RawSqlSource 对象else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource;
}

方法首先会调用XMLScriptBuilder#parseDynamicTags方法对当前 SQL 语句标签中的占位符进行解析,并判断是否为动态 SQL,实现如下:

protected MixedSqlNode parseDynamicTags(XNode node) {List<SqlNode> contents = new ArrayList<>();// 获取并处理所有的子标签NodeList children = node.getNode().getChildNodes();for (int i = 0; i < children.getLength(); i++) {// 构造对应的 XNode 对象,期间会尝试解析所有的 ${} 占位符XNode child = node.newXNode(children.item(i));if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {// 获取标签的 value 值String data = child.getStringBody("");TextSqlNode textSqlNode = new TextSqlNode(data);// 基于是否存在未解析的占位符 ${} 判断是否是动态 SQLif (textSqlNode.isDynamic()) {contents.add(textSqlNode);// 标记为动态 SQLisDynamic = true;} else {contents.add(new StaticTextSqlNode(data));}}// 如果子标签是 element 类型,则必定是一个动态 SQLelse if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628String nodeName = child.getNode().getNodeName();// 获取 nodeName 对应的 NodeHandlerNodeHandler handler = nodeHandlerMap.get(nodeName);if (handler == null) {throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");}// 基于具体的 NodeHandler 处理动态 SQLhandler.handleNode(child, contents);isDynamic = true;}}// 封装 SqlNode 集合为 MixedSqlNode 对象return new MixedSqlNode(contents);
}

整个过程主要是遍历当前 SQL 语句标签的所有子标签,并依据当前子标签的类型分而治之,可以配合官方文档的动态 SQL 配置示例进行理解。如果当前子标签是一个具体的字符串或 CDATA 表达式(即 SQL 语句片段),则会获取字面值并依据是否包含未解析的${}占位符判断是否是动态 SQL,并封装成对应的 SqlNode 对象。SqlNode 是一个接口,用于封装定义的动态 SQL 节点和文本节点,包含多个实现类,该接口及其具体实现类留到后面针对性介绍。如果当前子标签是一个具体的 XML 标签,则必定是一个动态 SQL 配置,此时会依据标签名称选择对应的 NodeHandler 对节点进行处理。标签与具体 NodeHandler 的映射关系如下:

// org.apache.ibatis.scripting.xmltags.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());
}

下面以 ForEachHandler 为例进行说明,其余 NodeHandler 实现与之类似。ForEachHandler 类对应动态 SQL 中的<foreach/>标签,这是一个我十分喜欢的标签,可以很方便的动态构造较长的条件语句。NodeHandler 中仅声明了NodeHandler#handleNode这一个方法,ForEachHandler 针对该方法的实现如下:

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {// 解析 <foreach/> 的子标签MixedSqlNode mixedSqlNode = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);// 获取属性配置String collection = nodeToHandle.getStringAttribute("collection");String item = nodeToHandle.getStringAttribute("item");String index = nodeToHandle.getStringAttribute("index");String open = nodeToHandle.getStringAttribute("open");String close = nodeToHandle.getStringAttribute("close");String separator = nodeToHandle.getStringAttribute("separator");// 封装为 ForEachSqlNode 对象ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);targetContents.add(forEachSqlNode);
}

方法首先会调用前面介绍的 XMLScriptBuilder#parseDynamicTags 方法对占位符进行嵌套解析,然后获取标签相关属性配置,并构造 ForEachSqlNode 对象。ForEachSqlNode 类在后面介绍 SqlNode 类时会进行介绍,这里先不展开。

介绍完了 XMLScriptBuilder#parseDynamicTags 方法,我们继续回到该方法调用的地方,即 XMLScriptBuilder#parseScriptNode 方法。接下来,MyBatis 会依据 XMLScriptBuilder#parseDynamicTags 方法的解析和判定结果分别创建对应的 SqlSource 对象。如果是动态 SQL,则采用 DynamicSqlSource 进行封装,否则采用 RawSqlSource 进行封装。

至此,我们在映射文件或注解中定义的 SQL 语句就被解析封装成对应的 SqlSource 对象驻于内存之中。接下来,MyBatis 会依据配置创建对应的 KeyGenerator 对象,这个留到后面解析 <selectKey/> 子标签时再进行说明。最后,MyBatis 会将 SQL 语句标签封装成 MappedStatement 对象,记录到 Configuration#mappedStatements 属性中。

解析 include 子标签

MyBatis 在解析 SQL 语句标签时会包含对 <include/> 子标签的解析。前面我们曾分析了 <sql/> 标签,该标签用于配置可复用的 SQL 语句片段,而 <include/> 标签则是用来引用已定义的 <sql/> 标签配置。对于 <include/> 子标签的解析由 XMLIncludeTransformer#applyIncludes 方法实现,该方法首先会尝试获取记录在 Configuration 配置对象中记录的 <properties/> 等属性变量,然后调用重载的 XMLIncludeTransformer#applyIncludes 方法进行解析,实现如下:

public void applyIncludes(Node source) {Properties variablesContext = new Properties();Properties configurationVariables = configuration.getVariables();Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);this.applyIncludes(source, variablesContext, false);
}private void applyIncludes(Node source, final Properties variablesContext, boolean included) {/* 注意:最开始进入本方法时,source 参数对应的标签并不是 <include/>,而是 <select/> 这类标签 */// 处理 <include/> 标签if (source.getNodeName().equals("include")) {// 获取 refid 指向的 <sql/> 标签对象的深拷贝Node toInclude = this.findSqlFragment(this.getStringAttribute(source, "refid"), variablesContext);// 获取 <include/> 标签下的 <property/> 子标签列表,与 variablesContext 合并返回新的 Properties 对象Properties toIncludeContext = this.getVariablesContext(source, variablesContext);// 递归处理,这里的 included 参数为 truethis.applyIncludes(toInclude, toIncludeContext, true);if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {toInclude = source.getOwnerDocument().importNode(toInclude, true);}// 替换 <include/> 标签为 <sql/> 标签source.getParentNode().replaceChild(toInclude, source);while (toInclude.hasChildNodes()) {// 将 <sql/> 的子标签添加到 <sql/> 标签的前面toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);}// 删除 <sql/> 标签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));}}// 遍历处理当前 SQL 语句标签的子标签NodeList children = source.getChildNodes();for (int i = 0; i < children.getLength(); i++) {// 递归调用this.applyIncludes(children.item(i), variablesContext, included);}} else if (included&& (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)&& !variablesContext.isEmpty()) {// 替换占位符为 variablesContext 中对应的配置值,这里替换的是引用 <sql/> 标签中定义的语句片段中对应的占位符source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));}
}

第一次进入上述方法时,参数 source 对应的并不是一个<include/>标签,由参数可以推导出它是一个具体的 SQL 语句标签(即Node.ELEMENT_NODE),所以方法一开始会进入中间的else if代码块(注意,最开始调用XMLIncludeTransformer#applyIncludes方法时传递的 included 参数为 false,所以对于 SQL 语句标签下面的Node.TEXT_NODE类型字面值是不会进入最后一个else if代码块的)。在这里会获取 SQL 语句标签的所有子标签,并递归调用XMLIncludeTransformer#applyIncludes方法进行处理,只有当存在<include/>标签时才会继续执行下面的逻辑。如果当前是<include/>标签,则会尝试获取 refid 属性,并对属性值中的占位符进行解析替换,然后从Configuration#sqlFragments属性中获取 id 对应的<sql/>标签节点的深拷贝对象。相关实现如下:

private Node findSqlFragment(String refid, Properties variables) {// 解析带有 ${} 占位符的字符串,将其中的占位符变量替换成 variables 中对应的属性值refid = PropertyParser.parse(refid, variables);  // 注意:这里替换的并不是 <sql/> 语句片段中的占位符refid = builderAssistant.applyCurrentNamespace(refid, true);try {// 从 Configuration#sqlFragments 中获取 id 对应的 <sql/> 标签XNode nodeToInclude = configuration.getSqlFragments().get(refid);// 返回节点的深拷贝对象return nodeToInclude.getNode().cloneNode(true);} catch (IllegalArgumentException e) {throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);}
}

接下来会尝试获取 <include/> 标签下的 <property/> 子标签列表,并与入参的 variablesContext 对象合并成为新的 Properties 对象。然后,递归调用 XMLIncludeTransformer#applyIncludes 方法,此时第三个参数 included 为 true,意味着会进入最后一个 else if 代码块。此时会依据之前解析得到的属性值替换引入的 SQL 语句片段中的占位符,最终将对应的 <include/> 标签替换成对应解析后的 <sql/> 标签,记录到当前所隶属的 SQL 语句标签中。

解析 selectKey 子标签

标签 <selectKey/> 用于为不支持自动生成自增主键的数据库或驱动提供主键生成支持,以及获取插入操作返回的主键值。该标签的解析位于 XMLStatementBuilder#processSelectKeyNodes 方法中,实现如下:

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {// 获取所有的 <selectKey/> 标签List<XNode> selectKeyNodes = context.evalNodes("selectKey");// 解析 <selectKey/> 标签if (configuration.getDatabaseId() != null) {this.parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());}this.parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);// 移除 <selectKey/> 标签this.removeSelectKeyNodes(selectKeyNodes);
}private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {// 遍历处理所有的 <selectKey/> 标签for (XNode nodeToHandle : list) {String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;String databaseId = nodeToHandle.getStringAttribute("databaseId");// 验证数据库类型是否匹配,忽略不匹配的 <selectKey/> 标签if (this.databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {this.parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);}}
}

上述方法执行过程如代码注释,核心步骤位于XMLStatementBuilder#parseSelectKeyNode方法中。该方法首先会获取<selectKey/>相应的属性配置,然后封装定义的 SQL 语句为 SqlSource 对象,最后将整个<selectKey/>配置封装成为 MappedStatement 对象记录到Configuration#mappedStatements属性中,同时创建对应的 KeyGenerator 对象,记录到Configuration#keyGenerators属性中。方法XMLStatementBuilder#parseSelectKeyNode实现如下:

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {/* 获取相应属性配置 */// 解析结果类型配置String resultType = nodeToHandle.getStringAttribute("resultType");Class<?> resultTypeClass = this.resolveClass(resultType);// 解析 statementType 配置,默认使用 PreparedStatementStatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 标签 <selectKey/> 生成结果应用的目标属性,多个用逗号分隔个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 对象(用于封装配置的 SQL 语句,此时的 SQL 语句仍不可执行),默认使用的是 XMLLanguageDriverSqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);SqlCommandType sqlCommandType = SqlCommandType.SELECT;// 创建 SQL 对应的 MappedStatement 对象,记录到 Configuration#mappedStatements 属性中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);// 创建对应的 KeyGenerator,记录到 Configuration#keyGenerators 属性中configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

在前面解析 SQL 语句标签时包含如下代码段,用于决策 KeyGenerator 具体实现。如果当前标签配置了<selectKey/>标签则优先从Configuration#keyGenerators属性中获取,也就是上面记录到该属性中的 SelectKeyGenerator 对象。对于未配置<selectKey/>标签的 SQL 语句标签,则会判断当前标签是否有设置 useGeneratedKeys 属性(即使用 JDBC 的 getGeneratedKeys 方法取出由数据库内部生成的主键),或者判断当前是否有设置全局的 useGeneratedKeys 属性,以及当前是否是 INSERT 数据库操作类型以决策具体的 KeyGenerator 实现。

// 解析对应的 KeyGenerator 实现,用于生成填充 keyProperty 属性指定的列值
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 当前 SQL 语句标签下存在 <selectKey/> 配置,直接获取对应的 SelectKeyGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);
}
// 当前 SQL 语句标签下不存在 <selectKey/> 配置
else {// 依据当前标签的 useGeneratedKeys 配置,或全局的 useGeneratedKeys 配置,以及是否是 INSERT 方法来决定具体的 keyGenerator 实现// 属性 useGeneratedKeys 仅对 INSERT 和 UPDATE 有用,使用 JDBC 的 getGeneratedKeys 方法取出由数据库内部生成的主键keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

对于 KeyGenerator 接口来说,包含三种实现类:Jdbc3KeyGenerator、NoKeyGenerator 和 SelectKeyGenerator。该接口的定义如下:

public interface KeyGenerator {/** 前置操作, order=BEFORE */void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);/** 后置操作, order=AFTER */void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

对于这三种实现而言,其中 NoKeyGenerator 虽然实现了该接口,但是对应方法体全部都是空实现,所以没什么可以分析的,我们接下来分别探究一下 Jdbc3KeyGenerator 和 SelectKeyGenerator 的实现。
(字数受限,请去主页查找下篇)!!!!!!!!!!!!!!!

一个mapper接口有多个mapper.xml 文件_MyBatis 源码解析:映射文件的加载与解析(上)相关推荐

  1. [转]/tomcat/conf/server.xml配置文件的源码解析

    备注: 在把Java项目直接放到/tomcat/webapps目录下时,server.xml的代码是不变的,端口为8080 你可以通过修改这个8080端口进行配置,以及配置<host>里面 ...

  2. Tomcat7.0源码分析——server.xml文件的加载与解析

    前言 作为Java程序员,对于Tomcat的server.xml想必都不陌生.本文基于Tomcat7.0的Java源码,对server.xml文件是如何加载和解析进行分析. 加载过程分析 Bootst ...

  3. Spring 加载、解析applicationContext.xml 流程

    概要 Spring 框架使用了BeanFactory 进行加载 xml 和生成 bean 实例.下面我们分析下Spring加载xml文件的过程. spring 版本是最新的 4.3.9 release ...

  4. Spring源码分析(1) —— 从Xml的加载到解析

    题外话: 接口&多态 我有一辆自行车,每天骑着它去上班 package com.zhao.SpringIoc;public class Bike {public void go() {Syst ...

  5. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由"yuanrw"分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读 ...

  6. web.xml 里context-param 、listener、 filter、servlet 加载顺序

    1.web.xml中context-param .listener. filter.servlet 加载顺序与其位置无关,不是listener在context-param 前面就先加载listener ...

  7. C++判断一个数字是否是某个数字的阶乘(附完整源码)

    C++判断一个数字是否是某个数字的阶乘算法 C++判断一个数字是否是某个数字的阶乘算法完整源码(定义,实现,main函数测试) C++判断一个数字是否是某个数字的阶乘算法完整源码(定义,实现,main ...

  8. java如何将数据保存为xml6_用Java实现可保存状态的数据库生成XML树,源码来了(9)...

    用Java实现可保存状态的数据库生成XML树,源码来了(9) 时间:2006/7/19 5:38:30 作者:佚名 人气:30 6.3.2.Servlet源码 1.RefreshServlet.jav ...

  9. [html] 写一个左中右的满屏布局,左右固定220px,中间自适应并且要优先加载

    [html] 写一个左中右的满屏布局,左右固定220px,中间自适应并且要优先加载 <!DOCTYPE html> <html lang="en"> < ...

最新文章

  1. CVPR2020:基于层次折叠的跳跃式注意网络点云完成
  2. 性能超FPN!北大、阿里等提多层特征金字塔网络
  3. 图论分析方法gretna_基于磁共振的多模态分析对血管性认知障碍患者脑网络的研究...
  4. PyQt5 技术篇-调用字体对话框(QFontDialog)获取字体,控件设置字体。
  5. python docx 设置表格字体和格式_python-docx修改已存在的Word文档的表格的字体格式方法...
  6. python---异常处理结构
  7. 关于大型asp.net应用系统的架构—如何做到高性能高可伸缩性
  8. Ubuntu 16.04粘贴板增强工具Diodon
  9. apache支持.htaccess
  10. 网众远程修改ip、dns
  11. Pro Git读书笔记 - 分支
  12. 各大浏览器的最小字体与默认字体
  13. 开源:通用的日志分析工具(LogViewer)
  14. 计算机开机黑屏风扇转,电脑风扇转但是开不了机怎么办_电脑风扇转但是黑屏...
  15. 信奥中的数学:卷积、傅立叶变换
  16. 程序猿段子_程序员段子
  17. html好看特效代码,教你制作漂亮的HTML代码特效贴
  18. R和Rstudio 下载安装
  19. 利用H5的canvas画一个时钟
  20. 迅捷画图中套用流程图模板编辑方法介绍

热门文章

  1. NAB展会新闻:微软和媒体娱乐合作伙伴在Windows Azure平台上发展数字供应链解决方案...
  2. Oracle多表树型查询备忘
  3. 【廖雪峰官方网站/Java教程】多线程(3)
  4. 【重点!DP】LeetCode 115. Distinct Subsequences
  5. 人工智能领域的会议和期刊的出版社和地址
  6. PHP 7天前的时间戳
  7. 开发Servlet的方法(2)
  8. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---05
  9. python 之发送邮件服务[原著] 海瑞博客
  10. Android 数据库 在使用 update更新的时候,总是无法更新,还没有异常抛出!