MyBatis 源码解析:映射文件的加载与解析

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

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

方法 XMLMapperBuilder#parse 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 方法实现了对映射文件解析的核心步骤,与配置文件解析的实现方式一样,这也是一个调度方法,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。

实现类 名称 描述
BlockingCache 阻塞式缓存装饰器 采用 ConcurrentHashMap 对象记录每个 key 对应的可重入锁对象,当执行 getObject 操作时会尝试获取 key 对应的锁对象,并应用带有超时机制的加锁操作,在获取到缓存值之后会释放锁。
FifoCache 先进先出缓存装饰器 采用双端队列记录 key 进入缓存的顺序,队列的大小默认是 1024,当执行 putObject 操作时,如果当前缓存的对象数超过缓存大小,则会触发 FIFO 策略。
LruCache 近期最少使用缓存装饰器 通过 LinkedHashMap 类型的 keyMap 属性记录缓存中每个 key 的使用情况,并使用 eldestKey 对象记录当前最少被使用的 key,当缓存达到容量上限时将会移除使用频率最小的缓存项。
LoggingCache 日志功能缓存装饰器 并不是如其字面意思是对缓存增加日志记录功能,该缓存装饰器中增加了两个属性:requests 和 hits,分别用于记录缓存被访问的次数和缓存命中的次数,并提供了 getHitRatio 方法以获取当前缓存的命中率。
ScheduledCache 周期性清理缓存装饰器 用于定期对缓存进行执行 clear 操作,其中定义了两个属性:clearInterval 和 lastClear,分别用来记录执行清理的时间间隔(默认为 1 小时)和最近一次执行清理的时间戳,在每次操作缓存时都会触发对缓存当前清理状态的检查,如果间隔时间达到设置值,就会触发对缓存的清理操作。
SerializedCache 序列化支持缓存装饰器 用于对缓存值进行序列化处理后再进行缓存,当我们执行 putObject 操作时,该装饰器会基于 java 的序列化机制对缓存值进行序列化(序列化结果存储在内存),反之,当我们执行 getObject 操作时,如果对应的缓存值存在,则会对该值执行反序列化再返回。
SoftCache 软引用缓存装饰器 通过软引用 Entry 内部类对缓存值进行修饰,值的生命周期受 GC 操作影响。
WeakCache 弱引用缓存装饰器 实现同 SoftCache,只是这里使用的是弱引用。
SynchronizedCache 同步缓存装饰器 通过在相应的缓存操作方法前都增加了 synchronized 关键字修饰,类似于 HashTable 的实现方式。
TransactionalCache 事务缓存装饰器 主要用于二级缓存,留到下一篇介绍缓存模块设计时再进行分析。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,以及 WEAK
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval"); // 刷新间隔,单位:毫秒
Integer size = context.getIntAttribute("size"); // 缓存大小,默认为 1024
boolean 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 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 实体类之间的属性映射关系:

1
2
3
4
5
6
7
8
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 方法实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 对象,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 方法中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 方法执行,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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.id
id = 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 集合中,方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.id
id = 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 属性值:

1
2
3
4
5
6
7
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 类进行处理,该方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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/> 配置,直接获取对应的 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;
}

// 创建 SQL 语句标签对应的 SqlSource 对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取具体的 Statement 类型,默认使用 PreparedStatement
StatementType 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/> 的标签 ID
String 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
// 基于是否存在未解析的占位符 ${} 判断是否是动态 SQL
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
// 标记为动态 SQL
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
}
// 如果子标签是 element 类型,则必定是一个动态 SQL
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
// 获取 nodeName 对应的 NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 基于具体的 NodeHandler 处理动态 SQL
handler.handleNode(child, contents);
isDynamic = true;
}
}
// 封装 SqlNode 集合为 MixedSqlNode 对象
return new MixedSqlNode(contents);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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 针对该方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 方法进行解析,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 参数为 true
this.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/> 标签节点的深拷贝对象。相关实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 方法中,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void parseSelectKeyNode(
String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {

/* 获取相应属性配置 */

// 解析结果类型配置
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = this.resolveClass(resultType);
// 解析 statementType 配置,默认使用 PreparedStatement
StatementType 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 语句仍不可执行),默认使用的是 XMLLanguageDriver
SqlSource 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 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 解析对应的 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。该接口的定义如下:

1
2
3
4
5
6
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 的实现。

  • Jdbc3KeyGenerator

首先来看 Jdbc3KeyGenerator 实现类,这是一个用于获取数据库自增主键值的实现版本。Jdbc3KeyGenerator 的 Jdbc3KeyGenerator#processBefore 方法是一个空实现,主要实现逻辑位于 Jdbc3KeyGenerator#processAfter 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
this.processBatch(ms, stmt, parameter);
}

public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
// 获取 keyProperty 属性配置,用于指定生成结果所映射的目标属性,可能存在多个
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
// 调用 Statement#getGeneratedKeys 方法获取数据库自动生成的主键
try (ResultSet rs = stmt.getGeneratedKeys()) {
// 获取 ResultSet 元数据信息
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
// 使用主键值填充 parameter 目标属性
this.assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}

private void assignKeys(Configuration configuration,
ResultSet rs,
ResultSetMetaData rsmd,
String[] keyProperties,
Object parameter) throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// Multi-param or single param with @Param
this.assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList
&& !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// Multi-param or single param with @Param in batch operation
this.assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
} else {
// Single param without @Param
this.assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}

上述实现的主要逻辑就是获取数据库自增的主键值,并设置到用户传递实参(parameter)的相应属性中。用户指定的实参可以是一个具体的实体类对象、Map 对象,以及集合类型,上述方法会依据入参类型分而治之。以 t_user 这张数据表为例,假设有如下插入语句:

1
2
3
4
5
<insert id="insert" parameterType="org.zhenchao.mybatis.entity.User" useGeneratedKeys="true" keyProperty="id">
insert into t_user (username, password, age, phone, email)
values (#{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR},
#{age,jdbcType=INTEGER}, #{phone,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR})
</insert>

那么 MyBatis 在执行插入时会先获取到数据库的自增 ID 值,并填充到 User 对象中。这里最终会调用 Jdbc3KeyGenerator#assignKeysToParam 方法填充目标属性值,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void assignKeysToParam(Configuration configuration,
ResultSet rs,
ResultSetMetaData rsmd,
String[] keyProperties,
Object parameter) throws SQLException {
// 将 Object 类型参数转换成相应的集合类型
Collection<?> params = collectionize(parameter);
if (params.isEmpty()) {
return;
}
// 遍历为每个目标属性配置创建对应的 KeyAssigner 分配器
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
}
// 遍历填充目标属性
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
}
Object param = iterator.next();
// 基于 KeyAssigner 使用自增 ID 填充目标属性
assignerList.forEach(x -> x.assign(rs, param));
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign
protected void assign(ResultSet rs, Object param) {
if (paramName != null) {
// If paramName is set, param is ParamMap
param = ((ParamMap<?>) param).get(paramName);
}
// 创建实参对应的 MetaObject 对象,以实现对于实参对象的反射操作
MetaObject metaParam = configuration.newMetaObject(param);
try {
if (typeHandler == null) {
// 创建目标属性对应的类型处理器
if (metaParam.hasSetter(propertyName)) {
Class<?> propertyType = metaParam.getSetterType(propertyName);
typeHandler = typeHandlerRegistry.getTypeHandler(
propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition)));
} else {
throw new ExecutorException("No setter found for the keyProperty '"
+ propertyName + "' in '" + metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
if (typeHandler == null) {
// Error?
} else {
// 设置目标属性值
Object value = typeHandler.getResult(rs, columnPosition);
metaParam.setValue(propertyName, value);
}
} catch (SQLException e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
e);
}
}

如果对上述实现不能很好的理解,建议 debug 一下,能够豁然开朗。

  • SelectKeyGenerator

SelectKeyGenerator 主要适用于那些不支持自动生成自增主键的数据库类型,从而为这些数据库生成主键值。SelectKeyGenerator 实现了 keyGenerator 接口中定义的全部方法,但是这些方法本质上均将请求直接委托给 SelectKeyGenerator#processGeneratedKeys 方法处理,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
// 获取 keyProperty 属性配置,用于指定生成结果所映射的目标属性,可能存在多个
String[] keyProperties = keyStatement.getKeyProperties();
final Configuration configuration = ms.getConfiguration();
// 创建实参 parameter 对应的 MetaObject 对象,便于反射操作
final MetaObject metaParam = configuration.newMetaObject(parameter);
// 创建 SQL 执行器,并执行 <selectKey/> 中定义的 SQL 语句
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
List<Object> values = keyExecutor.query(
keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);

/* 处理 <selectKey/> 的返回值,填充目标属性 */

if (values.size() == 0) {
throw new ExecutorException("SelectKey returned no data.");
} else if (values.size() > 1) {
throw new ExecutorException("SelectKey returned more than one value.");
} else {
// 创建主键值对应的 MetaObject 对象
MetaObject metaResult = configuration.newMetaObject(values.get(0));
// 单列主键的情况
if (keyProperties.length == 1) {
if (metaResult.hasGetter(keyProperties[0])) {
this.setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
}
// 没有 getter 方法,尝试直接获取属性值
else {
// no getter for the property - maybe just a single value object, so try that
this.setValue(metaParam, keyProperties[0], values.get(0));
}
}
// 多列主键的情况,依次从主键对象中获取对应的属性记录到用户参数对象中
else {
this.handleMultipleProperties(keyProperties, metaParam, metaResult);
}
}
}
} catch (ExecutorException e) {
throw e;
} catch (Exception e) {
throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
}
}

SelectKeyGenerator 会执行 <selectKey/> 中定义的 SQL 语句,拿到具体的返回值依据 keyProperty 配置填充目标属性。

封装 SQL 语句

上面的分析中曾遇到 SqlNode 和 SqlSource 这两个接口,本小节将对这两个接口及其实现类做一个分析。在这之前我们需要简单了解一下这两个接口各自的作用,由前面的分析我们知道对于一个 SQL 语句标签而言,最后会被封装成为一个 MappedStatement 对象,而标签中定义的 SQL 语句则由 SqlSource 进行表示,SqlNode 则用来定义动态 SQL 节点和文本节点等。

SqlNode

由点及面,我们先来看一下 SqlNode 的相关实现。SqlNode 是一个接口,其中仅声明了一个 SqlNode#apply 方法,接口定义如下:

1
2
3
4
public interface SqlNode {
/** 基于传递的实参,解析动态 SQL 节点 */
boolean apply(DynamicContext context);
}

围绕该接口的实现类的 UML 图如下:

image

下面逐一对 SqlNode 的实现类进行分析。

  • MixedSqlNode

首先来看一下前面多次遇到的 MixedSqlNode,它通过一个 MixedSqlNode#contents 集合属性记录包含的 SqlNode 对象,其 MixedSqlNode#apply 方法会遍历该集合并应用记录的各个 SqlNode 对象的 SqlNode#apply 方法,实现比较简单。

  • StaticTextSqlNode

与 MixedSqlNode 实现类似的还包括 StaticTextSqlNode 类。该类采用一个 String 类型的 StaticTextSqlNode#text 属性记录非动态的 SQL 节点,其 StaticTextSqlNode#apply 方法直接调用 DynamicContext#appendSql 方法将记录的 SQL 节点添加到一个 StringBuilder 类型属性中。该属性用于记录 SQL 语句片段,当我们最后调用 DynamicContext#getSql 方法时会调用该属性的 toString 方法拼接记录的 SQL 片段,返回最终完整的 SQL 语句。

  • TextSqlNode

TextSqlNode 用于封装包含占位符 ${} 的动态 SQL 节点,前面在分析 SQL 语句标签时也曾遇到。该实现类的 TextSqlNode#apply 方法定义如下:

1
2
3
4
5
6
7
public boolean apply(DynamicContext context) {
// BindingTokenParser 是内部类,基于 DynamicContext#bindings 中的属性解析 SQL 语句中的占位符
GenericTokenParser parser = this.createParser(new BindingTokenParser(context, injectionFilter));
// 解析并记录 SQL 片段到 DynamicContext 中
context.appendSql(parser.parse(text));
return true;
}

GenericTokenParser 的执行逻辑我们之前遇到过多次,它主要用来查找指定标识的占位符(这里的占位符是 ${}),并基于指定的 TokenHandler 对解析到的占位符变量进行处理。TextSqlNode 内部实现了 TokenHandler 解析器(即 BindingTokenParser),该解析器基于 DynamicContext#bindings 属性中记录的参数值解析 SQL 语句中的占位符,并将解析结果记录到 DynamicContext 对象中。

  • VarDeclSqlNode

VarDeclSqlNode 对应动态 SQL 中的 <bind/> 标签,该标签可以从 OGNL 表达式中创建一个变量并将其绑定到上下文中,官方文档中关于该标签的使用示例如下:

1
2
3
4
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG WHERE title LIKE #{pattern}
</select>

VarDeclSqlNode 定义了 VarDeclSqlNode#nameVarDeclSqlNode#expression 两个属性,分别与 <bind/> 标签的属性对应。该实现类的 VarDeclSqlNode#apply 方法完成了对 OGNL 表达式的解析,并将解析得到的真实值记录到 DynamicContext#bindings 属性中:

1
2
3
4
5
6
7
public boolean apply(DynamicContext context) {
// 解析 OGNL 表达式对应的值
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 绑定到上下文中,name 对应属性 <bind/> 标签的 name 属性配置
context.bind(name, value);
return true;
}
  • IfSqlNode

IfSqlNode 对应动态 SQL 的 <if/> 标签,这也是我们频繁使用的条件标签。IfSqlNode 的属性定义如下:

1
2
3
4
5
6
/** 用于解析 <if/> 标签的 test 表达式 */
private final ExpressionEvaluator evaluator;
/** 记录 <if/> 标签中的 test 表达式 */
private final String test;
/** 记录 <if/> 标签的子标签 */
private final SqlNode contents;

相应的 IfSqlNode#apply 实现会首先调用 ExpressionEvaluator#evaluateBoolean 方法判定 IfSqlNode#test 属性记录的表达式是否为 true,如果为 true 则应用记录的子标签的 SqlNode#apply 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean apply(DynamicContext context) {
// 检测 test 表达式是否为 true
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 执行子标签的 apply 方法
contents.apply(context);
return true;
}
return false;
}

// org.apache.ibatis.scripting.xmltags.ExpressionEvaluator#evaluateBoolean
public boolean evaluateBoolean(String expression, Object parameterObject) {
// 获取 OGNL 表达式对应的值
Object value = OgnlCache.getValue(expression, parameterObject);
// 转换为 boolean 类型返回
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
  • ChooseSqlNode

ChooseSqlNode 对应动态 SQL 中的 <choose/> 标签,我们通常利用此标签配合 <when/><otherwise/> 标签实现 switch 功能,具体使用方式可以参考官方示例。实现层面,MyBatis 并没有定义 WhenSqlNode 和 OtherwiseSqlNode 类与另外两个标签相对应,而是采用 IfSqlNode 表示 <when/> 标签,采用 MixedSqlNode 表示 <otherwise/> 标签。ChooseSqlNode 类的属性和 ChooseSqlNode#apply 方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 对应 <otherwise/> 标签,采用 {@link MixedSqlNode} 表示 */
private final SqlNode defaultSqlNode;
/** 对应 <when/> 标签,采用 {@link IfSqlNode} 表示 */
private final List<SqlNode> ifSqlNodes;

public boolean apply(DynamicContext context) {
// 遍历应用 <when/> 标签,一旦成功一个就返回
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// 所有的 <when/> 都不满足,执行 <otherwise/> 标签
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
  • TrimSqlNode

TrimSqlNode 对应 <trim/> 标签,用于处理动态 SQL 拼接在一些条件下出现不完整 SQL 的情况,具体使用可以参考官方示例。该实现类的属性和 TrimSqlNode#apply 方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 记录 <trim/> 标签的子标签 */
private final SqlNode contents;
/** 期望追加的前缀字符串 */
private final String prefix;
/** 期望追加的后缀字符串 */
private final String suffix;
/** 如果 <trim/> 包裹的 SQL 语句为空,则删除指定前缀 */
private final List<String> prefixesToOverride;
/** 如果 <trim/> 包裹的 SQL 语句为空,则删除指定后缀 */
private final List<String> suffixesToOverride;

public boolean apply(DynamicContext context) {
// 创建 FilteredDynamicContext 对象,封装上下文
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// 应用子标签的 apply 方法
boolean result = contents.apply(filteredDynamicContext);
// 处理前缀和后缀
filteredDynamicContext.applyAll();
return result;
}

TrimSqlNode 中定义了内部类 FilteredDynamicContext,它是对上下文对象 DynamicContext 的封装,其 FilteredDynamicContext#applyAll 方法实现了对不完整 SQL 的处理。该方法调用 FilteredDynamicContext#applyPrefixFilteredDynamicContext#applySuffix 方法分别处理 SQL 的前缀和后缀,并将处理完后的 SQL 片段记录到上下文对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
// 全部转换成大写
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
// 处理前缀
this.applyPrefix(sqlBuffer, trimmedUppercaseSql);
// 处理后缀
this.applySuffix(sqlBuffer, trimmedUppercaseSql);
}
// 添加解析后的结果到 delegate 中
delegate.appendSql(sqlBuffer.toString());
}

方法 FilteredDynamicContext#applyPrefixFilteredDynamicContext#applySuffix 的实现思路相同,这里以 FilteredDynamicContext#applyPrefix 方法为例进行说明。该方法会遍历指定的前缀并判断当前 SQL 片段是否以包含的前缀开头,是的话则会删除该前缀,如果指定了 prefix 属性则会在 SQL 语句片段前面追加对应的前缀值。WhereSqlNode 和 SetSqlNode 均由 TrimSqlNode 派生而来,实现比较简单,不多作撰述。

  • ForEachSqlNode

最后再来看一下 ForEachSqlNode 类,该类对应 <foreach/> 标签,前面我们曾介绍了相关的 ForEachHandler 类实现。ForEachSqlNode 类是所有 SqlNode 实现类中最复杂的一个,其主要的属性定义如下(建议参考官方文档进行理解):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 标识符 */
public static final String ITEM_PREFIX = "__frch_";
/** 用于判断循环的终止条件 */
private final ExpressionEvaluator evaluator;
/** 迭代的集合表达式 */
private final String collectionExpression;
/** 记录子标签 */
private final SqlNode contents;
/** open 标识 */
private final String open;
/** close 标识 */
private final String close;
/** 循环过程中,各项之间的分隔符 */
private final String separator;
/** index 是迭代的次数,item 是当前迭代的元素 */
private final String item;
private final String index;

ForEachSqlNode 中定义了两个内部类:FilteredDynamicContext 和 PrefixedContext。

FilteredDynamicContext 由 DynamicContext 派生而来,其中稍复杂的实现是 FilteredDynamicContext#appendSql 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
// 替换 item 为 __frch_item_index
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
// 替换 itemIndex 为 __frch_itemIndex_index
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
// 追加 #{} 标识
return "#{" + newContent + "}";
});

delegate.appendSql(parser.parse(sql));
}

实际上这里还是之前多次碰到的 GenericTokenParser 解析占位符的套路(这里的占位符是 #{}),对应的 TokenHandler#handleToken 方法会将 item 替换成 __frch_item_index 的形式,拼接的过程由 ForEachSqlNode#itemizeItem 方法实现:

1
2
3
4
private static String itemizeItem(String item, int i) {
// 返回 __frch_item_i 的形式
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}

PrefixedContext 也派生自 DynamicContext 类,在遍历集合拼接时主要用于封装一个由指定前缀和集合元素组成的基本元组,具体实现比较简单。

回到 ForEachSqlNode 类本身,继续来看 ForEachSqlNode#apply 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
// 解析集合 OGNL 表达式对应的值,返回值对应的迭代器
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
// 添加 open 前缀标识
this.applyOpen(context);
int i = 0;
// 迭代处理集合
for (Object o : iterable) {
// 备份一下上下文对象
DynamicContext oldContext = context;
// 第一次遍历,或未指定分隔符
if (first || separator == null) {
context = new PrefixedContext(context, "");
}
// 其它情况
else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// 如果是 Map 类型,将 key 和 value 记录到 DynamicContext#bindings 属性中
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
this.applyIndex(context, mapEntry.getKey(), uniqueNumber);
this.applyItem(context, mapEntry.getValue(), uniqueNumber);
}
// 将当前索引值和元素记录到 DynamicContext#bindings 属性中
else {
this.applyIndex(context, i, uniqueNumber);
this.applyItem(context, o, uniqueNumber);
}
// 应用子标签的 apply 方法
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
// 恢复上下文对象
context = oldContext;
i++;
}
// 添加 close 后缀标识
this.applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}

上述方法的执行过程阅读起来没什么压力,但就是不知道具体在做什么事情。下面我们以批量查询用户信息表 t_user 中的多个用户信息为例来走一遍上述方法的执行过程,对应的动态查询语句定义如下:

1
2
3
4
5
6
<select id="selectByIds" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT * FROM t_user WHERE id IN
<foreach collection="ids" index="idx" item="itm" open="(" close=")" separator=",">
#{itm}
</foreach>
</select>

假设我们现在希望查询 id 为 1 和 2 的两个用户,执行流程可以表述如下:

  1. 解析获取到集合表达式对应的集合迭代器对象,这里对应的是一个 List 类型集合的迭代器,其中包含了 1 和 2 两个元素;
  2. 调用 ForEachSqlNode#applyOpen 方法添加 OPEN 标识符,这里即 (
  3. 进入 for 循环,因为是第一次遍历,所以会创建 prefix 参数为空字符串的 PrefixedContext 对象;
  4. 这里集合类型中封装的是 Long 类型(不是 Map 类型):
    • 调用 ForEachSqlNode#applyIndex 方法,记录键值对 (idx, 0)(__frch_idx_0, 0)DynamicContext#bindings 属性中;
    • 调用 ForEachSqlNode#applyItem 方法,记录键值对 (itm, 1)(__frch_itm_0, 1)DynamicContext#bindings 中;
  5. 应用子标签的 SqlNode#apply 方法,这里会触发 FilteredDynamicContext#appendSql 方法解析占位符 #{itm}#{__frch_itm_0},此时生成的 SQL 语句片段已然成为 SELECT * FROM t_user WHERE id IN ( #{__frch_itm_0}
  6. 进入 for 循环的第二次遍历,此时 first 变量已经置为 false,且这里设置了分隔符,所以执行 new PrefixedContext(context, separator) 创建上下文对象;
  7. 这里集合类型同样是 Long 类型(不是 Map 类型):
    • 调用 ForEachSqlNode#applyIndex 方法,记录键值对 (idx, 1)(__frch_idx_1, 1)DynamicContext#bindings 属性中;
    • 调用 ForEachSqlNode#applyItem 方法,记录键值对 (itm, 2)(__frch_itm_1, 2)DynamicContext#bindings 属性中;
  8. 应用子标签的 SqlNode#apply 方法,这里会触发 FilteredDynamicContext#appendSql 方法解析占位符 #{itm}#{__frch_itm_1},此时生成的 SQL 语句片段已然成为 SELECT * FROM t_user WHERE id IN ( #{__frch_itm_0}, #{__frch_itm_1}
  9. for 循环结束,调用 ForEachSqlNode#applyClose 追加 CLOSE 标识符,这里即 )

最后解析得到的 SQL 为 SELECT * FROM t_user WHERE id IN ( #{__frch_itm_0} , #{__frch_itm_1} )。希望通过这样一个过程辅助读者进行理解,如果还是云里雾里可以 debug 一下整个过程。

SqlSource

前面介绍了 SqlSource 用于表示映射文件或注解定义的 SQL 语句标签中的 SQL 语句,但是这里的 SQL 语句并不是可执行的,其中可能包含一些动态占位符。SqlSource 接口的定义如下:

1
2
3
4
5
6
7
8
9
10
11
public interface SqlSource {

/**
* 基于传入的参数返回可执行的 SQL 语句
*
* @param parameterObject 用户传递的实参
* @return
*/
BoundSql getBoundSql(Object parameterObject);

}

围绕该接口的实现类的 UML 图如下:

image

其中,RawSqlSource 用于封装静态定义的 SQL 语句;DynamicSqlSource 用于封装动态定义的 SQL 语句;ProviderSqlSource 则用于封装注解形式定义的 SQL 语句。不管是动态还是静态的 SQL 语句,经过处理之后都会封装成为 StaticSqlSource 对象,其中包含的 SQL 语句是可以直接执行的。

考虑 MyBatis 目前还是主推 XML 的配置使用方式,所以不打算对 ProviderSqlSource 展开说明。在开始分析剩余三个实现类之前,需要先对这几个类共享的一个核心组件 SqlSourceBuilder 进行分析。SqlSourceBuilder 继承自 BaseBuilder,主要用于解析前面经过 SqlNode#apply 方法处理的 SQL 语句中的占位符属性,同时将占位符替换成 ? 字符串。

SqlSourceBuilder 中仅定义了一个 SqlSourceBuilder#parse 方法,实现了对占位符 #{} 中属性的解析,并将占位符替换成 ?。最终将解析得到的 SQL 语句和相关参数封装成 StaticSqlSource 对象返回。方法 SqlSourceBuilder#parse 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
public SqlSource parse(
String originalSql, // 经过 SqlNode#apply 方法处理后的 SQL 语句
Class<?> parameterType, // 用户传递的实参类型
Map<String, Object> additionalParameters) { // 记录形参与实参之间的对应关系,即 SqlNode#apply 方法处理之后记录在 DynamicContext#bindings 属性中的键值对
// 创建 ParameterMappingTokenHandler 对象,用于解析 #{} 占位符
ParameterMappingTokenHandler handler =
new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql); // SELECT * FROM t_user WHERE id IN ( ? , ? )
// 构造 StaticSqlSource 对象,其中封装了被替换成 ? 的 SQL 语句,以及参数对应的 ParameterMapping 集合
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

该方法的实现还是我们熟悉的套路,获取指定占位符中的属性,然后交由对应的 TokenHandler 进行处理。SqlSourceBuilder 定义了 ParameterMappingTokenHandler 内部类,这是一个具体的 TokenHandler 实现,该内部类同时还继承自 BaseBuilder 抽象类,对应的 ParameterMappingTokenHandler#handleToken 方法实现如下;

1
2
3
4
5
6
7
public String handleToken(String content) { // 占位符中定义的属性,例如 __frch_itm_0
// 调用 buildParameterMapping 方法构造当前 content 对应的 ParameterMapping 对象,并记录到 parameterMappings 集合中
// ParameterMapping{property='__frch_itm_0', mode=IN, javaType=class java.lang.Long, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
parameterMappings.add(this.buildParameterMapping(content));
// 全部返回 ? 字符串
return "?";
}

上述方法会调用 ParameterMappingTokenHandler#buildParameterMapping 方法构造实参 content (占位符中的属性)对应的 ParameterMapping 对象,并记录到 ParameterMappingTokenHandler#parameterMappings 属性中,同时返回 ? 占位符将原始 SQL 中对应的占位符全部替换成 ? 字符。这里我们以前面 SqlNode#apply 方法解析得到的 SELECT * FROM t_user WHERE id IN ( #{__frch_itm_0} , #{__frch_itm_1} ) 为例,该 SQL 语句经过 SqlSourceBuilder#parse 方法处理之后会被解析成 SELECT * FROM t_user WHERE id IN ( ? , ? ) 的形式封装到 StaticSqlSource 对象中。对应的 ParameterMappingTokenHandler#parameterMappings 参数内容如下:

1
2
ParameterMapping{property='__frch_itm_0', mode=IN, javaType=class java.lang.Long, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='__frch_itm_1', mode=IN, javaType=class java.lang.Long, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

了解了 SqlSourceBuilder 的作用,我们回头来看 DynamicSqlSource 的实现就会比较容易,DynamicSqlSource 实现了 SqlSource 接口中声明的 SqlSource#getBoundSql 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public BoundSql getBoundSql(Object parameterObject) {
// 构造上下文对象
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 应用 SqlNode#apply 方法(树型结构,会遍历应用树中各个节点的 SqlNode#apply 方法),各司其职追加 SQL 片段到上下文中
rootSqlNode.apply(context);
// 创建 SqlSourceBuilder 对象,解析占位符属性,并将 SQL 语句中的 #{} 占位符替换成 ? 字符
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); // 解析用户实参类型
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); // 解析并封装结果为 StaticSqlSource 对象
// 基于 SqlSourceBuilder 解析结果和实参创建 BoundSql 对象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 将 DynamicContext#bindings 中的参数信息复制到 BoundSql#additionalParameters 属性中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}

该方法最终会将解析得到的 SQL 语句,以及相应的参数全部封装到 BoundSql 对象中返回,具体过程可以参考上述代码注释。

相对于 DynamicSqlSource 来说,RawSqlSource 的 RawSqlSource#getBoundSql 方法实现就要简单了许多。RawSqlSource 直接将请求委托给了 StaticSqlSource 处理,本质上就是基于用户传递的参数来构造 BoundSql 对象。对应 SQL 的解析则放置在构造方法中,在构造方法中会调用 RawSqlSource#getSql 方法获取对应的 SQL 定义,同样基于 SqlSourceBuilder 对原始 SQL 语句进行解析,封装成 StaticSqlSource 对象记录到属性中,在实际运行时只要填充参数即可。这也是很容易理解的,毕竟对于静态 SQL 来说,它的模式在整个应用程序运行过程中是不变的,所以在系统初始化时完成解析操作,后续可以直接拿来使用,但是对于动态 SQL 来说,SQL 语句的具体模式取决于用户传递的参数,需要在运行时实时解析。

绑定 Mapper 接口

饶了一大圈,看起来我们似乎完成了对映射文件的加载和解析工作,实际上我们确实完成了对映射文件的解析,但是光解析还是不够的,实际开发中我们对于这些定义在映射文件中的 SQL 语句的调用一般都是通过 Mapper 接口完成。所以还需要建立映射文件与具体 Mapper 接口之间的映射关系,这一过程由 XMLMapperBuilder#bindMapperForNamespace 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void bindMapperForNamespace() {
// 获取当前映射文件的 namespace 配置
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 解析 namespace 对应的 Mapper 接口类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
if (boundType != null) {
// 当前 Mapper 还未加载
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
// 记录当前已经加载的 namespace 标识到 Configuration#loadedResources 属性中
configuration.addLoadedResource("namespace:" + namespace);
// 注册对应的 Mapper 接口到 Configuration#mapperRegistry 属性中(对应 MapperRegistry)
configuration.addMapper(boundType);
}
}
}
}

上述方法首先会获取对应映射文件的命名空间,然后构造命名空间字面量对应的 Class 类型,并记录到 Configuration 对象中。这里本质上调用的是 MapperRegistry#addMapper 方法执行注册操作,MapperRegistry 的实现之前已经分析过,这里就不再重复说明。

处理解析失败的标签

在前面分析解析过程时,对于一些解析异常的标签会记录到 Configuration 对象的相应属性中,包括 SQL 语句标签、<resultMap/> 标签,以及 <cache-ref/> 标签。需要说明的是这些记录的标签不一定全是解析异常所致,有些标签的解析存在依赖关系,如果 A 依赖于 B,在解析 A 时 B 还未被解析,MyBatis 则会将标签 A 记录起来,等到最后再尝试解析。在映射文件解析过程的最后会再次尝试对这些标签进行解析,如下面的代码所示:

1
2
3
4
5
6
// 处理解析失败的 <resultMap/> 标签
this.parsePendingResultMaps();
// 处理解析失败的 <cache-ref/> 标签
this.parsePendingCacheRefs();
// 处理解析失败的 SQL 语句标签
this.parsePendingStatements();

这些再次触发解析的方法在实现上都是一个思路,就是从 Configuration 对象中获取解析失败的标签对象集合,然后遍历执行相应的解析方法,前面已经对这些标签的解析过程进行了分析,不再重复。

总结

至此,我们算是真正完成了对映射文件的加载与解析工作,也基本上完成了 MyBatis 框架的初始化过程,接下来可以创建 SqlSession 对象,并执行具体的数据库操作。在下一篇中,我们将一起来分析 MyBatis 执行 SQL 语句的具体过程实现,包括获取 SQL 语句、绑定参数、执行数据库操作,以及结果集映射等操作。

参考

  1. MyBatis 官方文档
  2. MyBatis 技术内幕