十五、探究MyBatis 结果集映射机制背后的秘密(上)
我们看到 <resultMap>
标签会被解析成 ResultMap 对象,其中定义了 ResultSet 与 Java 对象的映射规则,简单来说,也就是一行数据记录如何映射成一个 Java 对象,这种映射机制是 MyBatis 作为 ORM 框架的核心功能之一。
ResultMap 只是定义了一个静态的映射规则,那在运行时,MyBatis 是如何根据映射规则将 ResultSet 映射成 Java 对象的呢?当 MyBatis 执行完一条 select 语句,拿到 ResultSet 结果集之后,会将其交给关联的 ResultSetHandler 进行后续的映射处理。
ResultSetHandler 是一个接口,其中定义了三个方法,分别用来处理不同的查询返回值:
java
public interface ResultSetHandler {
// 将ResultSet映射成Java对象
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
// 将ResultSet映射成游标对象
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
// 处理存储过程的输出参数
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
在 MyBatis 中只提供了一个 ResultSetHandler 接口实现,即 DefaultResultSetHandler。下面我们就以 DefaultResultSetHandler 为中心,介绍 MyBatis 中 ResultSet 映射的核心流程。
结果集处理入口
你如果有 JDBC 编程经验的话,应该知道在数据库中执行一条 Select 语句通常只能拿到一个 ResultSet,但这只是我们最常用的一种查询数据库的方式,其实数据库还支持同时返回多个 ResultSet 的场景,例如在存储过程中执行多条 Select 语句。MyBatis 作为一个通用的持久化框架,不仅要支持常用的基础功能,还要对其他使用场景进行全面的支持。
DefaultResultSetHandler 实现的 handleResultSets() 方法支持多个 ResultSet 的处理(单 ResultSet 的处理只是其中的特例),相关的代码片段如下:
java
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 用于记录每个ResultSet映射出来的Java对象
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 从Statement中获取第一个ResultSet,其中对不同的数据库有兼容处理逻辑,
// 这里拿到的ResultSet会被封装成ResultSetWrapper对象返回
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 获取这条SQL语句关联的全部ResultMap规则。如果一条SQL语句能够产生多个ResultSet,
// 那么在编写Mapper.xml映射文件的时候,我们可以在SQL标签的resultMap属性中配置多个
// <resultMap>标签的id,它们之间通过","分隔,实现对多个结果集的映射
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) { // 遍历ResultMap集合
ResultMap resultMap = resultMaps.get(resultSetCount);
// 根据ResultMap中定义的映射规则处理ResultSet,并将映射得到的Java对象添加到
// multipleResults集合中保存
handleResultSet(rsw, resultMap, multipleResults, null);
// 获取下一个ResultSet
rsw = getNextResultSet(stmt);
// 清理nestedResultObjects集合,这个集合是用来存储中间数据的
cleanUpAfterHandlingResultSet();
resultSetCount++; // 递增ResultSet编号
}
// 下面这段逻辑是根据ResultSet的名称处理嵌套映射,你可以暂时不关注这段代码,
// 嵌套映射会在后面详细介绍
...
// 返回全部映射得到的Java对象
return collapseSingleResultList(multipleResults);
}
这里我们先来看一下遍历多结果集时使用到的 getFirstResultSet() 方法和 getNextResultSet() 方法,这两个方法底层都是依赖 java.sql.Statement 的 getMoreResults() 方法和 getUpdateCount() 方法检测是否存在后续的 ResultSet 对象,检测成功之后,会通过 getResultSet() 方法获取下一个 ResultSet 对象。
这里获取到的 ResultSet 对象,会被包装成 ResultSetWrapper 对象返回。
ResultSetWrapper 主要用于封装 ResultSet 的一些元数据,其中记录了 ResultSet 中每列的名称、对应的 Java 类型、JdbcType 类型以及每列对应的 TypeHandler。
另外,ResultSetWrapper 可以将底层 ResultSet 的列与一个 ResultMap 映射的列进行交集,得到参与映射的列和未被映射的列,分别记录到 mappedColumnNamesMap 集合和 unMappedColumnNamesMap 集合中。这两个集合都是 Map<String, List<String>>
类型,其中最外层的 Key 是 ResultMap 的 id,Value 分别是参与映射的列名集合和未被映射的列名集合。
除了记录上述元数据以外,ResultSetWrapper 还封装了一套查询上述元数据的方法,例如,我们可以通过 getMappedColumnNames() 方法查询一个 ResultMap 映射了当前 ResultSet 的哪些列,还可以通过 getJdbcType()、getTypeHandler() 等方法查询指定列对应的 JdbcType、TypeHandler 等。
简单映射
了解了处理 ResultSet 的入口逻辑之后,下面我们继续来深入了解一下 DefaultResultSetHandler 是如何处理单个结果集的,这部分逻辑的入口是 handleResultSet() 方法,其中会根据第四个参数,也就是 parentMapping,判断当前要处理的 ResultSet 是嵌套映射,还是外层映射。
无论是处理外层映射还是嵌套映射,都会依赖 handleRowValues() 方法完成结果集的处理(通过方法名也可以看出,handleRowValues() 方法是处理多行记录的,也就是一个结果集)。
至于 handleRowValues() 方法,其中会通过 handleRowValuesForNestedResultMap() 方法处理包含嵌套映射的 ResultMap,通过 handleRowValuesForSimpleResultMap() 方法处理不包含嵌套映射的简单 ResultMap,如下所示:
java
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) { // 包含嵌套映射的处理流程
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else { // 简单映射的处理
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
这里我们重点来看 handleRowValuesForSimpleResultMap() 方法如何映射一个 ResultSet 的,该方法的核心步骤可总结为如下。
- 执行 skipRows() 方法跳过多余的记录,定位到指定的行。
- 通过 shouldProcessMoreRows() 方法,检测是否还有需要映射的数据记录。
- 如果存在需要映射的记录,则先通过 resolveDiscriminatedResultMap() 方法处理映射中用到的 Discriminator,决定此次映射实际使用的 ResultMap。
- 通过 getRowValue() 方法对 ResultSet 中的一行记录进行映射,映射规则使用的就是步骤 3 中确定的 ResultMap。
- 执行 storeObject() 方法记录步骤 4 中返回的、映射好的 Java 对象。
在开始详细介绍上述映射流程中的每一步之前,我们先来看一下贯穿整个映射过程的两个辅助对象------DefaultResultHandler 和 DefaultResultContext。
在 DefaultResultSetHandler 中维护了一个 resultHandler 字段(ResultHandler 接口类型)指向一个 DefaultResultHandler 对象,其核心作用是存储多个结果集映射得到的 Java 对象。
ResultHandler 接口有两个默认实现,如下图所示:

ResultHandler 接口继承图
DefaultResultHandler 实现的底层使用 ArrayList<Object>
存储映射得到的 Java 对象,DefaultMapResultHandler 实现的底层使用 Map<K, V>
存储映射得到的 Java 对象,其中 Key 是从结果对象中获取的指定属性的值,Value 就是映射得到的 Java 对象。
至于 DefaultResultContext 对象,它的生命周期与一个 ResultSet 相同,每从 ResultSet 映射得到一个 Java 对象都会暂存到 DefaultResultContext 中的 resultObject 字段,等待后续使用,同时 DefaultResultContext 还可以计算从一个 ResultSet 映射出来的对象个数(依靠 resultCount 字段统计)。
了解了 handleRowValuesForSimpleResultMap() 方法的核心步骤以及全部贯穿整个映射流程的辅助对象之后,下面我们开始深入每个步骤进行详细分析。
1. ResultSet 的预处理
有 MyBatis 使用经验的同学可能知道,我们可以通过 RowBounds 指定 offset、limit 参数实现分页的效果。这里的 skipRows() 方法就会根据 RowBounds 移动 ResultSet 的指针到指定的数据行,这样后续的映射操作就可以从这一行开始。
skipRows() 方法会检查 ResultSet 的属性,如果是 TYPE_FORWARD_ONLY 类型,则只能通过循环 + ResultSet.next() 方法(指针的逐行前移)定位到指定的数据行;反之,可以通过 ResultSet.absolute() 方法直接移动指针。
处理 RowBounds 的另一个方法是 shouldProcessMoreRows() 方法,其中会检查当前已经映射的行是否达到了 RowBounds.limit 字段指定的行数上限,如果达到,则返回 false,停止后续操作。当然,控制是否进行后续映射操作的条件还有 ResultSet.next() 方法(即结果集中是否还有数据)。
通过上述分析我们可以看出,通过 RowBounds 实现的分页功能实际上还是会将全部数据加载到 ResultSet 中,而不是只加载指定范围的数据,所以我们可以认为 RowBounds 实现的是一种"假分页"。这种"假分页"在数据量大的时候,性能就会很差,在处理大数据量分页时,建议通过 SQL 语句 where 条件 + limit 的方式实现分页。
2. 确定 ResultMap
在完成 ResultSet 的预处理之后,接下来会通过 resolveDiscriminatedResultMap() 方法处理 标签,确定此次映射操作最终使用的 ResultMap 对象。
为了更加方便和完整地描述 resolveDiscriminatedResultMap() 方法的核心流程,这里我们结合一个简单示例进行分析,比如,现在有一个 ResultSet 包含 id、name、classify、subClassify 四列,并且由 animalMap 来映射该 ResultSet,具体如下图所示:

< discriminator>处理示例图
通过 resolveDiscriminatedResultMap() 方法确定 ResultMap 的流程大致是这样的:
- 首先按照 animalMap 这个 ResultMap 映射这行记录,该行记录中的 classify 列值为 mammalia,根据其中定义的
<discriminator>
标签的配置,会选择使用 mammaliaMap 这个 ResultMap 对当前这条记录进行映射; - 接下来看 mammaliaMap 这个 ResultMap,其中的
<discriminator>
标签检查的是 subClassify 的列值,当前记录的 subClassify 列值为 human,所以会选择 humanMap 这个 ResultMap 映射当前这条记录,得到一个 Human 对象。
了解了上述基本流程之后,下面我们来看 resolveDiscriminatedResultMap() 方法的具体实现:
java
public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
// 用于维护处理过的ResultMap唯一标识
Set<String> pastDiscriminators = new HashSet<>();
// 获取ResultMap中的Discriminator对象,这是通过<resultMap>标签中的<discriminator>标签解析得到的
Discriminator discriminator = resultMap.getDiscriminator();
while (discriminator != null) {
// 获取当前待映射的记录中Discriminator要检测的列的值
final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
// 根据上述列值确定要使用的ResultMap的唯一标识
final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
if (configuration.hasResultMap(discriminatedMapId)) {
// 从全局配置对象Configuration中获取ResultMap对象
resultMap = configuration.getResultMap(discriminatedMapId);
// 记录当前Discriminator对象
Discriminator lastDiscriminator = discriminator;
// 获取ResultMap对象中的Discriminator
discriminator = resultMap.getDiscriminator();
// 检测Discriminator是否出现了环形引用
if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
break;
}
} else {
break;
}
}
// 返回最终要使用的ResultMap
return resultMap;
}
3. 创建映射结果对象
经过 resolveDiscriminatedResultMap() 方法解析,我们最终确定了当前记录使用哪个 ResultMap 进行映射。
接下来要做的就是按照 ResultMap 规则进行各个列的映射,得到最终的 Java 对象,这部分逻辑是在下面要介绍的 getRowValue() 方法完成的,其核心步骤如下:
- 首先根据 ResultMap 的 type 属性值创建映射的结果对象;
- 然后根据 ResultMap 的配置以及全局信息,决定是否自动映射 ResultMap 中未明确映射的列;
- 接着根据 ResultMap 映射规则,将 ResultSet 中的列值与结果对象中的属性值进行映射;
- 最后返回映射的结果对象,如果没有映射任何属性,则需要根据全局配置决定如何返回这个结果值,这里不同场景和配置,可能返回完整的结果对象、空结果对象或是 null。
下面是 getRowValue() 方法的核心实现:
java
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 根据ResultMap的type属性值创建映射的结果对象
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 根据ResultMap的配置以及全局信息,决定是否自动映射ResultMap中未明确映射的列
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 根据ResultMap映射规则,将ResultSet中的列值与结果对象中的属性值进行映射
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
// 如果没有映射任何属性,需要根据全局配置决定如何返回这个结果值,
// 这里不同场景和配置,可能返回完整的结果对象、空结果对象或是null
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
可以看到这里的第一步,也就是创建映射的结果对象,这部分逻辑位于 createResultObject() 方法中。这个方法中有两个关键步骤:一个是调用另一个 createResultObject() 重载方法来创建结果对象,另一个是通过 ProxyFactory 创建代理对象来处理延迟加载的属性。
由于我们重点分析的是简单 ResultSet 的映射流程,所以接下来我们重点看 createResultObject() 重载方法是如何创建映射结果对象的。
首先进行一些准备工作:获取 ResultMap 中 type 属性指定的结果对象的类型,并创建该类型对应的 MetaClass 对象;获取 ResultMap 中配置的 <constructor>
标签信息(也就是对应的 ResultMapping 对象集合),如果该信息不为空,则可以确定结果类型中的唯一构造函数。
然后再根据四种不同的场景,使用不同的方式创建结果对象,下面就是这四种场景的核心逻辑。
- 场景一,ResultSet 中只有一列,并且能够找到一个 TypeHandler 完成该列到目标结果类型的映射,此时可以直接读取 ResultSet 中的列值并通过 TypeHandler 转换得到结果对象。这部分逻辑是在 createPrimitiveResultObject() 方法中实现的,该场景多用于 Java 原始类型的处理。
- 场景二,如果 ResultMap 中配置了
<constructor>
标签,就会先解析<constructor>
标签中指定的构造方法参数的类型,并从待映射的数据行中获取对应的实参值,然后通过反射方式调用对应的构造方法来创建结果对象。这部分逻辑在 createParameterizedResultObject() 方法中实现。 - 场景三,如果不满足上述两个场景,则尝试查找默认构造方法来创建结果对象,这里使用前面介绍的 ObjectFactory.create() 方法实现,底层原理还是 Java 的反射机制。
- 场景四,最后会检测是否已经开启了自动映射功能,如果开启了,会尝试查找合适的构造方法创建结果对象。这里首先会查找 @AutomapConstructor 注解标注的构造方法,查找失败之后,则会尝试查找每个参数都有 TypeHandler 能与 ResultSet 列进行映射的构造方法,确定要使用的构造方法之后,也是通过 ObjectFactory 完成对象创建的。这部分逻辑在 createByConstructorSignature() 方法中实现。
4. 自动映射
创建完结果对象之后,下面就可以开始映射各个字段了。
在简单映射流程中,会先通过 shouldApplyAutomaticMappings() 方法检测是否开启了自动映射,主要检测以下两个地方。
- 检测当前使用的 ResultMap 是否配置了 autoMapping 属性,如果是,则直接根据该 autoMapping 属性的值决定是否开启自动映射功能。
- 检测 mybatis-config.xml 的
<settings>
标签中配置的 autoMappingBehavior 值,决定是否开启自动映射功能。autoMappingBehavior 指定 MyBatis 框架如何进行自动映射,该属性有三个可选值:①NONE,表示完全关闭自动映射功能;②PARTIAL,表示只会自动映射没有定义嵌套映射的 ResultMap;③FULL,表示完全打开自动映射功能,这里会自动映射所有 ResultMap。autoMappingBehavior 的默认值是 PARTIAL。
当确定当前 ResultMap 需要进行自动映射的时候,会通过 applyAutomaticMappings() 方法进行自动映射,其中的核心逻辑大致可描述为如下。
- 首先,从 ResultSetWrapper 中获取所有未映射的列名,然后逐个处理每个列名。通过列名获取对应的属性名称,这里会将列名转换为小写并截掉指定的前缀,得到相应的属性名称。
- 然后,检测结果对象中是否有上面得到的属性。如果属性不存在,则通过全局配置的 AutoMappingUnknownColumnBehavior 进行处理。如果属性存在,则检测该属性是否有合适的 TypeHandler;如果不存在合适的 TypeHandler,依旧是通过全局配置的 AutoMappingUnknownColumnBehavior 进行处理。
- 经过上述检测之后,就可以创建 UnMappedColumnAutoMapping 对象将该列与对应的属性进行关联。在 UnMappedColumnAutoMapping 中记录了列名、属性名以及相关的 TypeHandler。
- 最后,遍历上面得到 UnMappedColumnAutoMapping 集合,通过其中的 TypeHandler 读取列值并转换成相应的 Java 类型,再通过 MetaObject 设置到相应属性中。
这样就完成了自动映射的功能。
5. 正常映射
完成自动映射之后,MyBatis 会执行 applyPropertyMappings() 方法处理 ResultMap 中明确要映射的列,applyPropertyMappings() 方法的核心流程如下所示。
- 首先从 ResultSetWrapper 中明确需要映射的列名集合,以及 ResultMap 中定义的 ResultMapping 对象集合。
- 遍历全部 ResultMapping 集合,针对每个 ResultMapping 对象为 column 属性值添加指定的前缀,得到最终的列名,然后执行 getPropertyMappingValue() 方法完成映射,得到对应的属性值。
- 如果成功获取到了属性值,则通过结果对象关联的 MetaObject 对象设置到对应属性中。
在 getPropertyMappingValue() 方法中,主要处理了三种场景的映射:
- 第一种是基本类型的映射,这种场景直接可以通过 TypeHandler 从 ResultSet 中读取列值,并在转化之后返回;
- 第二种和第三种场景分别是嵌套映射和多结果集的映射,这两个逻辑相对复杂,在下一讲我们再详细介绍。
6. 存储对象
通过上述 5 个步骤,我们已经完成简单映射的处理,得到了一个完整的结果对象。接下来,我们就要通过 storeObject() 方法把这个结果对象保存到合适的位置。
这里处理的简单映射,如果是一个嵌套映射中的子映射,那么我们就需要将结果对象保存到外层对象的属性中;如果是一个普通映射或是外层映射的结果对象,那么我们就需要将结果对象保存到 ResultHandler 中。
明确了结果对象的存储位置之后,我们来看 storeObject() 方法的具体实现:
java
private void storeObject(...) throws SQLException {
if (parentMapping != null) {
// 嵌套查询或嵌套映射的场景,此时需要将结果对象保存到外层对象对应的属性中
linkToParents(rs, parentMapping, rowValue);
} else {
// 普通映射(没有嵌套映射)或是嵌套映射中的外层映射的场景,此时需要将结果对象保存到ResultHandler中
callResultHandler(resultHandler, resultContext, rowValue);
}
}
总结
这一讲我们重点介绍了结果集映射,这是 MyBatis 的核心实现之一。
首先我们介绍了 ResultSetHandler 接口以及 DefaultResultSetHandler 这个默认实现,并讲解了单个结果集映射的入口:handleResultSet() 方法。
接下来,我们继续深入,详细分析了 handleRowValuesForSimpleResultMap() 方法实现简单映射的核心步骤,其中涉及预处理 ResultSet、查找并确定 ResultMap、创建并填充映射结果对象、自动映射、正常映射、存储映射结果对象这六大核心步骤。
十六、探究MyBatis 结果集映射机制背后的秘密(下)
今天我们就紧接着上一讲,继续介绍 DefaultResultSetHandler 中关于嵌套映射、延迟加载以及多结果集处理的内容。
嵌套映射
处理简单映射只是所有映射处理逻辑中的一个分支,handleRowValues() 方法还有另一条分支是用来处理嵌套映射的,也就是 handleRowValuesForNestedResultMap() 方法。
handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如下所示。
- 通过 skipRows() 方法将 ResultSet 的指针指向目标行。
- 执行 shouldProcessMoreRows() 方法检测 ResultSet 中是否包含能继续映射的数据行,如果包含,就开始映射一个具体的数据行。
- 通过 resolveDiscriminatedResultMap() 方法处理 ResultMap 中的 Discriminator 对象,确定最终使用的 ResultMap 映射规则。
- 为当前处理的数据行生成 CacheKey。除了作为缓存中的 key 值外,CacheKey 在嵌套映射中也作为唯一标识来标识结果对象。
- 根据步骤 4 生成的 CacheKey 从 DefaultResultSetHandler.nestedResultObjects 集合中查询中间结果。nestedResultObjects 是一个 HashMap 集合,在处理嵌套映射过程中产生的全部中间对象,都会记录到这个 Map 中,其中的 Key 就是 CacheKey。
- 检测
<select>
标签中 resultOrdered 属性的配置,并根据 resultOrdered 的配置决定是否提前释放 nestedResultObjects 集合中的中间数据,避免在进行嵌套映射时出现内存不足的情况。 - 通过 getRowValue() 方法完成当前记录行的映射,得到最终的结果对象,其中还会将结果对象添加到 nestedResultObjects 集合中。
- 通过 storeObject() 方法将生成的结果对象保存到 ResultHandler 中。
在上述过程中,有很多步骤的实现已经在上一讲的简单映射部分介绍过了,例如,前三步中使用到的 skipRows()、shouldProcessMoreRows() 和 resolveDiscriminatedResultMap() 三个方法。所以,下面我们就从(第 4 步)创建 CacheKey 开始介绍。
1. 创建 CacheKey
创建 CacheKey 的核心逻辑在 createRowKey() 方法中 ,该方法构建 CacheKey 的过程是这样的:尝试使用 <idArg>
标签或 <id>
标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 <idArg>
标签或 <id>
标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。
无论是使用 <idArg>
、<id>
指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建,最终都是为了使 CacheKey 能够唯一标识结果对象。
2. 外层映射
完成 CacheKey 的创建之后,我们开始处理嵌套映射,整个处理过程的入口是 getRowValue() 方法。
因为嵌套映射涉及多层映射,这里我们先来关注外层映射的处理流程。
首先通过 createResultObject() 方法创建外层对象,再通过 shouldApplyAutomaticMappings() 方法检测是否开启自动映射来处理包含嵌套的映射。对于嵌套映射,只有 ResultMap 明确配置或是全局的 AutoMappingBehavior 配置为 FULL 的时候,才会开启自动映射。
如果发现开启了自动映射,则会指定 applyAutomaticMappings() 方法,处理 ResultMap 中未明确映射的列。然后再通过 applyPropertyMappings() 方法处理 ResultMap 中明确需要进行映射的列。applyAutomaticMappings() 方法和 applyPropertyMappings() 方法我们在上一讲中已经详细分析过了,这里就不再赘述。
到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个"部分映射"的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。
接下来就是与简单映射不一样的步骤了 。这里会先将"部分映射"的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap<String, Object>
类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的"部分映射"的结果对象。
然后通过 applyNestedResultMappings() 方法处理嵌套映射,在处理过程中,会从 ancestorObjects 集合中获取外层对象,并将嵌套映射产生的结果对象设置到外层对象的属性中。
处理完之后,就清理 ancestorObjects 集合,并将外层对象保存到 nestedResultObjects 集合中,等待后续的映射步骤继续使用。这里使用的 Key 就是前面创建的 CacheKey 对象。
了解了外层映射的核心步骤之后,下面我们一起来看一下 getRowValue() 方法的具体实现:
java
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
final String resultMapId = resultMap.getId();
Object rowValue = partialObject;
if (rowValue != null) { // 检测外层对象是否已经存在,如果存在,直接执行嵌套映射的逻辑
final MetaObject metaObject = configuration.newMetaObject(rowValue);
putAncestor(rowValue, resultMapId);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
} else { // 外层对象不存在,先生成外层映射的对象
// ResultLoaderMap与延迟加载相关
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建外层对象
rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 创建外层对象关联的MetaObject对象
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, true)) { // 自动映射
// 自动映射ResultMap中未明确映射的列
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 处理ResultMap中明确映射的列
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
// 将"部分构造"的外层对象添加到ancestorObjects集合中
putAncestor(rowValue, resultMapId);
// 处理嵌套映射,其中会从ancestorObjects集合中获取外层对象,并将嵌套映射的结果对象设置到外层对象的属性中
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
// 清理ancestorObjects集合,删除外层对象
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
// 将外层对象记录到nestedResultObjects集合中,等待后续使用
nestedResultObjects.put(combinedKey, rowValue);
}
}
return rowValue;
}
3. applyNestedResultMappings() 方法
通过对外层对象的处理我们可以知道,处理嵌套映射的核心在于 applyNestedResultMappings() 方法,其中会遍历 ResultMap 中的每个 ResultMapping 对象。
针对嵌套映射对应的 ResultMapping 对象进行特殊处理,其核心处理步骤如下。
- 确保 ResultMapping 对象的 nestedResultMapId 字段值不为空,该字段值保存了嵌套映射的 ResultMapId;同时还会检查 resultSet 字段是否为空,如果不为空,则是多结果集的映射,不是嵌套映射。
- 确定此次嵌套映射使用的 ResultMap 对象,这里依赖上一讲介绍的 resolveDiscriminatedResultMap() 方法。
- 处理循环引用的场景。如果存在循环引用的情况,则此次嵌套映射不会执行,直接重用已存在的嵌套对象即可。这里会先检查在 ancestorObjects 集合中是否已经存在嵌套对象,如果存在,就可以重用这个嵌套对象。
- 为嵌套对象创建 CacheKey。嵌套对象的 CacheKey 除了包含嵌套对象的信息,还会包含外层对象的 CacheKey 信息,这样才能得到一个全局唯一的 CacheKey 对象。
- 对外层对象的集合属性进行特殊处理。如果外层对象中用于记录当前嵌套对象的属性为 Collection 类型,且该属性未初始化,则这里会初始化该集合。
- 调用 getRowValue() 方法完成嵌套映射,得到嵌套对象。嵌套映射是支持嵌套多层的,这也就是产生 getRowValue() 方法递归的原因。
- 通过 linkObjects() 方法,将步骤 6 中映射得到的嵌套对象保存到外层对象的对应属性中,底层会依赖外层对象的 MetaObject 实现属性的设置。
延迟加载
MyBatis 中的"延迟加载"是指在查询数据库的时候,MyBatis 不会立即将完整的对象加载到服务内存中,而是在业务逻辑真正需要使用这个对象或使用到对象中某些属性的时候,才真正执行数据库查询操作,将完整的对象加载到内存中。
MyBatis 实现延迟加载的底层原理是动态代理 ,但并不是[《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》]中介绍的 JDK 动态代理,而是通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成。
这里我们简单说明一下,之所以不用 JDK 动态代理是因为 JDK 动态代理在生成代理对象的时候,要求目标类必须实现接口,而通过 MyBatis 映射产生的结果对象基本都是 POJO 对象,没有实现任何接口,所以 JDK 动态代理不适用。
下面我们先简单了解一下 cglib 和 javassist 这两个库的基本使用,这样才能看懂 MyBatis 延迟加载的逻辑。
1. cglib
cglib 实现动态代理的底层原理是字节码生成技术,具体就是使用字节码生成技术生成一个目标类的子类,然后在这个子类中进行方法重写,并在重写的方法中进行拦截,实现代理对象的相关功能。
既然使用生成子类的方式来实现动态代理,那根据 Java 的语法规则,final 关键字修饰的方法无法被子类覆盖,自然也就无法通过 cglib 实现代理,所以我们可以将 cglib 与 JDK 动态代理作为互补的两个方案一起使用,在 Spring 等很多开源框架中,也都会同时使用这两个代理生成方式。
那如何使用 cglib 实现动态代理的功能呢?下面我们就来看看 cglib 的基础使用,在 cglib 中有一个关键的接口------ Callback 接口,它有很多子接口,如下图所示:

Callback 接口继承关系图
这里我们重点关注 MethodInterceptor 接口,它可以实现方法拦截的功能,可参考下面这个简单的实现:
java
public class CglibProxyDemo implements MethodInterceptor {
// cglib中的Enhancer对象
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
// 代理类的父类
enhancer.setSuperclass(clazz);
// 添加Callback对象
enhancer.setCallback(this);
// 通过cglib动态创建子类实例并返回
return enhancer.create();
}
// intercept()方法中实现了方法拦截
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("before operation...");
// 调用父类中的方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("after operation...");
return result;
}
}
下面我们创建一个目标类------ CglibMainDemo,这也是整个示例的入口类,这里使用 CglibProxyDemo 创建 CglibMainDemo 的代理对象,并执行 method() 方法:
java
public class CglibMainDemo { // 父类,也是代理的目标类
public String method(String str) { // 被代理的目标方法
System.out.println(str);
return "CglibMainDemo:" + str;
}
public static void main(String[] args) {
CglibProxyDemo proxy = new CglibProxyDemo();
// 获取CglibMainDemo的代理对象
CglibMainDemo proxyImp = (CglibMainDemo) proxy.getProxy(CglibMainDemo.class);
// 执行代理对象的method()方法
String result = proxyImp.method("test");
System.out.println(result);
}
}
执行 CglibMainDemo 的 main() 方法,我们可以看到控制台中,CglibMainDemo.method() 方法前后都出现了相应的拦截输出(即 "before operation" 和 "after operation"),这也就实现了代理的效果。
2. Javassist
Javassist 是一个操纵 Java 字节码的类库,我们可以直接通过 Javassist 提供的 Java API 动态生成或修改类结构。Javassist 提供的 Java API 非常多,这里我们重点来看如何使用 javassist 创建动态代理。
首先创建 JavassistDemo 类,其中提供了一个属性和一个方法,它是代理的目标类,通过 javassist 创建的代理类会继承 JavassistDemo,如下示例:
java
public class JavassistDemo {
private String demoProperty = "demo-value"; // 字段
// demoProperty字段对应的getter/setter方法
public String getDemoProperty() {
return demoProperty;
}
public void setDemoProperty(String demoProperty) {
this.demoProperty = demoProperty;
}
// JavassistDemo的成员方法
public void operation() {
System.out.println("operation():" + this.demoProperty);
}
}
javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下:
java
public class JavassitMainDemo {
public static void main(String[] args) throws Exception {
// 创建ProxyFactory工厂实例,它负责动态生成JavassistDemo的子类
ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(JavassistDemo.class);
// 设置Filter,用于确定哪些方法调用需要被代理
factory.setFilter(new MethodFilter() {
public boolean isHandled(Method m) {
if (m.getName().equals("operation")) {
return true;
}
return false;
}
});
// 设置拦截处理逻辑,被拦截的方法会执行MethodHandler中的逻辑
factory.setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed,
Object[] args) throws Throwable {
System.out.println("before operation");
Object result = proceed.invoke(self, args);
System.out.println("after operation");
return result;
}
});
// 生成代理类,并根据代理类创建代理对象
Class<?> c = factory.createClass();
JavassistDemo javassistDemo = (JavassistDemo) c.newInstance();
// 执行operation()方法时会被拦截,进而执行代理逻辑
javassistDemo.operation();
System.out.println(javassistDemo.getDemoProperty());
}
}
执行 JavassitMainDemo 的 main() 方法,我们可以看到控制台在 JavassistDemo.operation() 方法的输出前后,都添加了相应的拦截输出(即 "before operation" 和 "after operation"),这就是我们想要的代理效果。
3. 辅助类
了解了 cglib 和 javassist 的基本原理之后,我们接下来再介绍一下 MyBatis 中与延迟加载相关的辅助类。
首先来看 ResultLoader 辅助类,它记录了一次延迟加载涉及的全部信息,其中包括延迟执行的 SQL 语句(boundSql 字段)、Sql 的实参(parameterObject 字段)、用于执行延迟 SQL 的线程池(executor 字段)以及延迟加载的对象类型(targetType 字段)等,这些信息在真正执行加载操作的时候,都是必要的信息。
ResultLoader 中核心的方法是 loadResult() 方法,其中会先通过 selectList() 方法执行 boundSql 这条延迟加载的 SQL 语句,得到的是一个 List<Object>
集合。在 selectList() 方法中会使用到 Executor 来执行 SQL 语句,这部分的核心内容我们将在后面的课时中详细分析。
接下来通过 ResultExtractor 从这个 List 集合中提取到延迟加载的真正对象,这里就涉及了 List 集合向 targetType 转换的一些逻辑:
- 如果目标类型就是 List,那 ResultExtractor 无须进行任何转换,直接返回 List;
- 如果目标类型是 Collection 子类、数组类型,则 ResultExtractor 会创建一个元素为 targetType 类型的集合对象,并将 List
<Object>
集合中元素项复制到其中; - 如果目标类型是一个普通 Java 对象,且上面得到的 List 长度为 1,则从 List 中获取到唯一的元素,并转换成 targetType 类型的对象并返回。
在一个 ResultMap 中,我们可以配置多个延迟加载的属性,这些属性与对应的 ResultLoader 的映射关系就记录在一个 ResultLoaderMap 对象中,ResultLoaderMap 中的 loaderMap 字段(HashMap<String, LoadPair>
类型)就用来维护这一关系,LoadPair 对象就是用来维护 ResultLoader 对象以及一些配置信息的。
ResultLoaderMap 提供了一个 load(String) 方法,参数是触发加载的属性名称,在执行这个方法的时候,会从 loaderMap 中获取(并删除)指定属性对应的 ResultLoader 对象,并调用其 load() 方法执行延迟 SQL,完成延迟加载。这个方法是在 cglib 和 javassist 生成的代理对象中被调用的(如下图所示),从而实现在使用某个属性时触发延迟加载的效果。

ResultLoaderMap.load() 方法的调用点
ResultLoaderMap 中还有一个 loadAll() 方法,这个方法会触发 loaderMap 中全部 ResultLoader 的 load() 方法,将所有延迟加载的对象都加载上来。
4. 代理工厂
为了同时接入 cglib 和 javassist 两种生成动态代理的方式,MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为,同时提供了下图中的两个实现类来接入上述两种生成方式:

ProxyFactory 的实现类图
ProxyFactory 接口中定义的核心方法是 createProxy() 方法,从名字也能看出这个方法是用来生成代理对象的。
在 JavassistProxyFactory 实现中,createProxy() 方法通过调用 EnhancedResultObjectProxyImpl 这个内部类的 createProxy() 方法来创建代理对象,具体实现与前文介绍的 JavassitMainDemo 类似,其中先是创建 javassist.util.proxy.ProxyFactory 对象,然后设置父类以及 MethodHandler 等信息,最后通过 javassist.util.proxy.ProxyFactory 的 create() 方法创建代理对象。
这里使用到 MethodHandler 实现就是 EnhancedResultObjectProxyImpl 本身,在其 invoke() 方法中首先会在 loaderMap 集合上加锁防止并发,然后通过 lazyLoader 集合的长度,判断是否存在延迟加载的属性。
在存在延迟加载属性的时候,会执行如下延迟加载操作。
- 首先,会优先检查全局的 aggressiveLazyLoading 配置和 lazyLoadTriggerMethods 配置。如果 aggressiveLazyLoading 配置为 true,或此次调用方法名称包含于 lazyLoadTriggerMethods 配置的方法名列表中,会立刻将该对象的全部延迟加载属性都加载上来,即触发 ResultLoaderMap.loadAll() 方法。
- 接下来,检查此次调用的方法是否为属性对应的 setter 方法,如果是,则该属性已经被赋值,无须再执行延迟加载操作,可以从 ResultLoaderMap 集合中删除该属性以及对应的 ResultLoader 对象。
- 最后,检测此次调用的方法是否为属性对应的 getter 方法,如果是,触发对应的 ResultLoader.load() 方法,完成延迟加载。
完成上述延迟加载操作之后,会释放 loaderMap 集合上的锁,然后调用目标对象的方法,完成真正的属性读写操作。
CglibProxyFactory 与 JavassistProxyFactory 的核心实现非常类似。CglibProxyFactory 中也定义了一个 EnhancedResultObjectProxyImpl 内部类,但是该内部类继承的是 cglib 中的 MethodHandler 接口,并通过 cglib 库的 API 实现代理逻辑。CglibProxyFactory 的具体实现,我就不赘述了,就留给你类比着分析了。
5. 延迟加载实现细节
了解了 MyBatis 中延迟加载的底层原理和相关辅助类,我们回到 DefaultResultSetHandler 中,看一下映射处理流程中与延迟加载相关的实现细节。
在 DefaultResultSetHandler.getPropertyMappingValue() 方法处理单个 ResultMapping 映射规则时候,会调用 getNestedQueryMappingValue() 方法处理嵌套映射,其中会有这么一段逻辑:
java
// 创建ResultLoader对象
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
// 根据是否延迟加载的配置决定value的值
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERRED;
} else {
value = resultLoader.loadResult();
}
我们可以清晰地看到,这里会检测该嵌套映射是否开启了延迟加载特性。如果开启了,则在 ResultLoaderMap 中记录延迟加载属性以及对应的 ResultLoader 对象,并返回 DEFERED 这个公共的占位符对象;如果未开启延迟加载特性,则直接执行嵌套查询,完成相应映射操作得到相应的结果对象。
另一个延迟加载的实现细节是在 createResultObject() 方法中,其中有如下代码片段:
java
for (ResultMapping propertyMapping : propertyMappings) {
// 检测所有ResultMapping规则,是否开启了延迟加载特性
if (propertyMapping.getNestedQueryId() != null &&
propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject,
lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
从上面这段代码中我们可以看到,如果检测到了延迟加载的属性,则会通过前面介绍的 ProxyFactory 为结果对象创建代理对象,然后在真正使用到延迟加载属性(即调用其 getter 方法)的时候,触发代理对象完成该属性的真正加载。
多结果集处理
在了解了简单映射、嵌套映射以及延迟加载的处理逻辑之后,下面我们再来介绍一下 MyBatis 中多结果集的处理逻辑。
在 getPropertyMappingValue() 方法中处理某个属性的映射时,有下面这个代码片段:
java
if (propertyMapping.getResultSet() != null) {
// 指定了resultSet属性,则等待后续结果集解析
addPendingChildRelation(rs, metaResultObject, propertyMapping);
return DEFERRED;
}
这段代码的含义是:这个属性的值来自后续的结果集(对应的结果集名称通过 resultSet 指定),后续结果集在这一时刻还未处理,所以会通过 addPendingChildRelation() 方法将该映射信息添加到 nextResultMaps 集合以及 pendingRelations 集合中暂存。
在 pendingRelations 集合中维护了 CacheKey 到 PendingRelation 对象之间的映射,PendingRelation 中维护了当前 ResultMapping 以及外层结果对象,nextResultMaps 集合中维护了 ResultSet 名称与当前 ResultMapping 对象的映射。
处理 nextResultMaps 集合的地方是在 handleResultSets() 方法中。在 handleResultSets() 方法完成全部 ResultMapping 映射之后,会开始遍历 nextResultMaps 集合,根据其中每个 ResultMapping 对象指定的 ResultMap 对后续的多个结果集进行映射,并将映射得到的结果对象设置到外层对象的相应属性中,相关的代码片段如下:
java
while (rsw != null && resultSetCount < resultSets.length) {
// 获取nextResultMaps中的ResultMapping对象
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
// 获取ResultMapping中指定的ResultMap映射规则
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
// 进行结果集映射,得到的结果对象会添加到外层结果对象的相应属性中
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt); // 继续获取下一个ResultSet
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
处理 pendingRelations 集合的地方是在 linkToParents() 方法中,该方法会从 pendingRelations 集合中获取结果对象所在外层对象,然后通过 linkObjects() 方法进行设置。
到此为止,MyBatis 中结果集映射的核心内容就介绍完了。
总结
紧接着上一讲的内容,我们继续介绍了 MyBatis 中关于结果集映射的相关知识点。
- 首先,重点讲解了 DefaultResultSetHandler 中嵌套映射的实现逻辑。
- 然后,介绍了 MyBatis 中延迟加载的实现细节,其中还详细说明了 MyBatis 实现延迟加载的两种方案以及 MyBatis 对这两种方案的封装和统一。
- 最后,简单分析了 MyBatis 对多结果集处理的实现。
除了上面介绍的这些核心映射方式之外,MyBatis 还支持游标、存储过程中的输出参数等方式返回查询结果,相关的逻辑也是在 DefaultResultSetHandler 中实现的,相关的方法就作为课后作业留给你自己分析了。
十七、StatementHandler:参数绑定、SQL 执行和结果映射的奠基者
StatementHandler 接口是 MyBatis 中非常重要的一个接口,其实现类完成 SQL 语句执行中最核心的一系列操作,这也是后面我们要介绍的 Executor 接口实现的基础。
StatementHandler 接口的定义如下图所示:

StatementHandler 接口中定义的方法
可以看到,其中提供了创建 Statement 对象(prepare() 方法)、为 SQL 语句绑定实参(parameterize() 方法)、执行单条 SQL 语句(query() 方法和 update() 方法)、批量执行 SQL 语句(batch() 方法)等多种功能。
下图展示了 MyBatis 中提供的所有 StatementHandler 接口实现类,以及它们的继承关系:

StatementHandler 接口继承关系图
今天这一讲我们就来详细分析该继承关系图中每个 StatementHandler 实现的核心逻辑。
RoutingStatementHandler
RoutingStatementHandler 这个 StatementHandler 实现,有点策略模式的意味。在 RoutingStatementHandler 的构造方法中,会根据 MappedStatement 中的 statementType 字段值,选择相应的 StatementHandler 实现进行创建,这个新建的 StatementHandler 对象由 RoutingStatementHandler 中的 delegate 字段维护。
RoutingStatementHandler 的构造方法如下:
java
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 下面就是根据MappedStatement的配置,生成一个相应的StatementHandler对
// 象,并设置到delegate字段中维护
switch (ms.getStatementType()) {
case STATEMENT:
// 创建SimpleStatementHandler对象
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
// 创建PreparedStatementHandler对象
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
// 创建CallableStatementHandler对象
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default: // 抛出异常
throw new ExecutorException("...");
}
}
在 RoutingStatementHandler 的其他方法中,都会委托给底层的 delegate 对象来完成具体的逻辑。
BaseStatementHandler
作为一个抽象类,BaseStatementHandler 只实现了 StatementHandler 接口的 prepare() 方法,其 prepare() 方法实现为新建的 Statement 对象设置了一些参数,例如,timeout、fetchSize 等。BaseStatementHandler 还新增了一个 instantiateStatement() 抽象方法给子类实现,来完成 Statement 对象的其他初始化操作。不过,BaseStatementHandler 中并没有实现 StatementHandler 接口中的数据库操作等核心方法。
了解了 BaseStatementHandler 对 StatementHandler 接口的实现情况之后,我们再来看一下 BaseStatementHandler 的构造方法,其中会初始化执行 SQL 需要的 Executor 对象 、为 SQL 绑定实参的 ParameterHandler 对象 以及生成结果对象的 ResultSetHandler 对象。这三个核心对象中,ResultSetHandler 对象我们已经在[《14 | 探究 MyBatis 结果集映射机制背后的秘密(上)》]中介绍过了,ParameterHandler 和 Executor 在后面会展开介绍。
1. KeyGenerator
这里需要关注的是 generateKeys() 方法,其中会通过 KeyGenerator 接口生成主键,下面我们就来看看 KeyGenerator 接口的相关内容。
我们知道不同数据库的自增 id 生成策略并不完全一样。例如,我们常见的 Oracle DB 是通过sequence 实现自增 id 的,如果使用自增 id 作为主键,就需要我们先获取到这个自增的 id 值,然后再使用;MySQL 在使用自增 id 作为主键的时候,insert 语句中可以不指定主键,在插入过程中由 MySQL 自动生成 id。KeyGenerator 接口支持 insert 语句执行前后获取自增的 id,分别对应 processBefore() 方法和 processAfter() 方法,下图展示了 MyBatis 提供的两个 KeyGenerator 接口实现:

KeyGenerator 接口继承关系图
Jdbc3KeyGenerator 用于获取数据库生成的自增 id(例如 MySQL 那种生成模式),其 processBefore() 方法是空实现,processAfter() 方法会将 insert 语句执行后生成的主键保存到用户传递的实参中。我们在使用 MyBatis 执行单行 insert 语句时,一般传入的实参是一个 POJO 对象或是 Map 对象,生成的主键会设置到对应的属性中;执行多条 insert 语句时,一般传入实参是 POJO 对象集合或 Map 对象的数组或集合,集合中每一个元素都对应一次插入操作,生成的多个自增 id 也会设置到每个元素的相应属性中。
Jdbc3KeyGenerator 中获取数据库自增 id 的核心代码片段如下:
java
// 将数据库生成的自增id作为结果集返回
try (ResultSet rs = stmt.getGeneratedKeys()) {
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
} else {
// 处理rs这个结果集,将生成的id设置到对应的属性中
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("...");
}
如果使用像 Oracle 这种不支持自动生成主键自增 id 的数据库时,我们可以使用 SelectkeyGenerator 来生成主键 id 。Mapper 映射文件中的<selectKey>
标签会被解析成 SelectkeyGenerator 对象,其中的 executeBefore 属性(boolean 类型)决定了是在 insert 语句执行之前获取主键,还是在 insert 语句执行之后获取主键 id。
SelectkeyGenerator 中的 processBefore() 方法和 processAfter() 方法都是通过 processGeneratedKeys() 这个私有方法获取主键 id 的,processGeneratedKeys() 方法会执行<selectKey>
标签中指定的 select 语句,查询主键信息,并记录到用户传入的实参对象的对应属性中,核心代码片段如下所示:
java
// 创建一个新的Executor对象来执行指定的select语句
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
// 拿到主键信息
List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
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对象
final MetaObject metaParam = configuration.newMetaObject(parameter);
MetaObject metaResult = configuration.newMetaObject(values.get(0));
if (keyProperties.length == 1) {
// 将主键信息记录到用户传入的实参对象中
if (metaResult.hasGetter(keyProperties[0])) {
setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
} else {
setValue(metaParam, keyProperties[0], values.get(0));
}
} else {
... // 多结果集的处理
}
}
2. ParameterHandler
介绍完 KeyGenerator 接口之后,我们再来看一下 BaseStatementHandler 中依赖的另一个辅助类------ ParameterHandler。
经过前面[《13 | 深入分析动态 SQL 语句解析全流程(下)》]介绍的一系列 SqlNode 的处理之后,我们得到的 SQL 语句(维护在 BoundSql 对象中)可能包含多个"?"占位符,与此同时,用于替换每个"?"占位符的实参都记录在 BoundSql.parameterMappings 集合中。
ParameterHandler 接口中定义了两个方法:一个是 getParameterObject() 方法,用来获取传入的实参对象;另一个是 setParameters() 方法,用来替换"?"占位符,这是 ParameterHandler 的核心方法。
DefaultParameterHandler 是 ParameterHandler 接口的唯一实现,其 setParameters() 方法会遍历 BoundSql.parameterMappings 集合,根据参数名称查找相应实参,最后会通过 PreparedStatement.set*() 方法与 SQL 语句进行绑定。setParameters() 方法的具体代码如下:
java
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
Object value;
String propertyName = parameterMapping.getProperty();
// 获取实参值
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获取TypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
// 底层会调用PreparedStatement.set*()方法完成绑定
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
SimpleStatementHandler
SimpleStatementHandler 是 StatementHandler 的具体实现之一,继承了 BaseStatementHandler 抽象类。SimpleStatementHandler 各个方法接收的是 java.sql.Statement 对象,并通过该对象来完成 CRUD 操作,所以在 SimpleStatementHandler 中维护的 SQL 语句不能存在"?"占位符,填充占位符的 parameterize() 方法也是空实现。
在 instantiateStatement() 这个初始化方法中,SimpleStatementHandler 会直接通过 JDBC Connection 创建 Statement 对象,这个对象也是后续 SimpleStatementHandler 其他方法的入参。
在 query() 方法实现中,SimpleStatementHandler 会直接通过上面创建的 Statement 对象,执行 SQL 语句,返回的结果集由 ResultSetHandler 完成映射,核心代码如下:
java
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {、
// 获取SQL语句
String sql = boundSql.getSql();
// 执行SQL语句
statement.execute(sql);
// 处理ResultSet映射,得到结果对象
return resultSetHandler.handleResultSets(statement);
}
queryCursor() 方法与 query() 方法实现类似,这里就不再赘述。
batch() 方法调用的是 Statement.addBatch() 方法添加批量执行的 SQL 语句,但并不是立即执行,而是等待 Statement.executeBatch() 方法执行时才会批量执行,这点你稍微注意一下即可。
至于 update() 方法,首先会通过 Statement.execute() 方法执行 insert、update 或 delete 类型的 SQL 语句,然后执行 KeyGenerator.processAfter() 方法查询主键并填充相应属性(processBefore() 方法已经在 prepare() 方法中执行过了),最后通过 Statement.getUpdateCount() 方法获取 SQL 语句影响的行数并返回。
PreparedStatementHandler
PreparedStatementHandler 是 StatementHandler 的具体实现之一,也是最常用的 StatementHandler 实现,它同样继承了 BaseStatementHandler 抽象类。PreparedStatementHandler 各个方法接收的是 java.sql.PreparedStatement 对象,并通过该对象来完成 CRUD 操作,在其 parameterize() 方法中会通过前面介绍的 ParameterHandler调用 PreparedStatement.set*() 方法为 SQL 语句绑定参数,所以在 PreparedStatementHandler 中维护的 SQL 语句是可以包含"?"占位符的。
在 instantiateStatement() 方法中,PreparedStatementHandler 会直接通过 JDBC Connection 的 prepareStatement() 方法创建 PreparedStatement 对象,该对象就是 PreparedStatementHandler 其他方法的入参。
PreparedStatementHandler 的 query() 方法、batch() 方法以及 update() 方法与 SimpleStatementHandler 的实现基本相同,只不过是把 Statement API 换成了 PrepareStatement API 而已。下面我们以 update() 方法为例进行简单介绍:
java
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute(); // 执行SQL语句,修改数据
int rows = ps.getUpdateCount(); // 获取影响行数
// 获取实参对象
Object parameterObject = boundSql.getParameterObject();
// 执行KeyGenerator
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows; // 返回影响行数
}
CallableStatementHandler
CallableStatementHandler 是处理存储过程的 StatementHandler 实现,其 instantiateStatement() 方法会通过 JDBC Connection 的 prepareCall() 方法为指定存储过程创建对应的 java.sql.CallableStatement 对象。在 parameterize() 方法中,CallableStatementHandler 除了会通过 ParameterHandler 完成实参的绑定之外,还会指定输出参数的位置和类型。
在 CallableStatementHandler 的 query()、queryCursor()、update() 方法中,除了处理 SQL 语句本身的结果集(ResultSet 结果集或是影响行数),还会通过 ResultSetHandler 的 handleOutputParameters() 方法处理输出参数,这是与 PreparedStatementHandler 最大的不同。下面我们以 query() 方法为例进行简单分析:
java
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
CallableStatement cs = (CallableStatement) statement;
cs.execute(); // 执行存储过程
// 处理存储过程返回的结果集
List<E> resultList = resultSetHandler.handleResultSets(cs);
// 处理输出参数,可能修改resultList集合
resultSetHandler.handleOutputParameters(cs);
// 返回最后的结果对象
return resultList;
}
总结
这一讲我们重点讲解了 MyBatis 中的 StatementHandler 接口及其核心实现,StatementHandler 接口中定义了执行一条 SQL 语句的核心方法。
- 首先,分析了 RoutingStatementHandler 实现,它可以帮助我们选择真正的 StatementHandler 实现类。
- 接下来,介绍了 BaseStatementHandler 这个抽象类的实现,同时还详细阐述了其中使用到的 KeyGenerator 和 ParameterHandler。
- 最后,又介绍了 SimpleStatementHandler、PreparedStatementHandler 等实现,它们基于 JDBC API 接口,实现了完整的 StatementHandler 功能。
十八、Executor才是执行SQL语句的幕后推手(上)
今天我们就紧接着上一讲,继续介绍 DefaultResultSetHandler 中关于嵌套映射、延迟加载以及多结果集处理的内容。
嵌套映射
处理简单映射只是所有映射处理逻辑中的一个分支,handleRowValues() 方法还有另一条分支是用来处理嵌套映射的,也就是 handleRowValuesForNestedResultMap() 方法。
handleRowValuesForNestedResultMap() 方法处理嵌套映射的核心流程如下所示。
- 通过 skipRows() 方法将 ResultSet 的指针指向目标行。
- 执行 shouldProcessMoreRows() 方法检测 ResultSet 中是否包含能继续映射的数据行,如果包含,就开始映射一个具体的数据行。
- 通过 resolveDiscriminatedResultMap() 方法处理 ResultMap 中的 Discriminator 对象,确定最终使用的 ResultMap 映射规则。
- 为当前处理的数据行生成 CacheKey。除了作为缓存中的 key 值外,CacheKey 在嵌套映射中也作为唯一标识来标识结果对象。
- 根据步骤 4 生成的 CacheKey 从 DefaultResultSetHandler.nestedResultObjects 集合中查询中间结果。nestedResultObjects 是一个 HashMap 集合,在处理嵌套映射过程中产生的全部中间对象,都会记录到这个 Map 中,其中的 Key 就是 CacheKey。
- 检测
<select>
标签中 resultOrdered 属性的配置,并根据 resultOrdered 的配置决定是否提前释放 nestedResultObjects 集合中的中间数据,避免在进行嵌套映射时出现内存不足的情况。 - 通过 getRowValue() 方法完成当前记录行的映射,得到最终的结果对象,其中还会将结果对象添加到 nestedResultObjects 集合中。
- 通过 storeObject() 方法将生成的结果对象保存到 ResultHandler 中。
在上述过程中,有很多步骤的实现已经在上一讲的简单映射部分介绍过了,例如,前三步中使用到的 skipRows()、shouldProcessMoreRows() 和 resolveDiscriminatedResultMap() 三个方法。所以,下面我们就从(第 4 步)创建 CacheKey 开始介绍。
1. 创建 CacheKey
创建 CacheKey 的核心逻辑在 createRowKey() 方法中 ,该方法构建 CacheKey 的过程是这样的:尝试使用 <idArg>
标签或 <id>
标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义 <idArg>
标签或 <id>
标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。
无论是使用 <idArg>
、<id>
指定的列名和列值来创建 CacheKey 对象,还是使用全部的列名和列值来创建,最终都是为了使 CacheKey 能够唯一标识结果对象。
2. 外层映射
完成 CacheKey 的创建之后,我们开始处理嵌套映射,整个处理过程的入口是 getRowValue() 方法。
因为嵌套映射涉及多层映射,这里我们先来关注外层映射的处理流程。
首先通过 createResultObject() 方法创建外层对象,再通过 shouldApplyAutomaticMappings() 方法检测是否开启自动映射来处理包含嵌套的映射。对于嵌套映射,只有 ResultMap 明确配置或是全局的 AutoMappingBehavior 配置为 FULL 的时候,才会开启自动映射。
如果发现开启了自动映射,则会指定 applyAutomaticMappings() 方法,处理 ResultMap 中未明确映射的列。然后再通过 applyPropertyMappings() 方法处理 ResultMap 中明确需要进行映射的列。applyAutomaticMappings() 方法和 applyPropertyMappings() 方法我们在上一讲中已经详细分析过了,这里就不再赘述。
到此为止,处理外层映射的步骤其实与处理简单映射的步骤基本一致,但不同的是:外层映射此时得到的并不是一个完整的对象,而是一个"部分映射"的对象,因为只填充了一部分属性,另一部分属性将由后面得到的嵌套映射的结果对象填充。
接下来就是与简单映射不一样的步骤了 。这里会先将"部分映射"的结果对象添加到 ancestorObjects 集合中暂存,ancestorObjects 是一个 HashMap<String, Object>
类型,key 是 ResultMap 的唯一标识(即 id 属性值),value 为外层的"部分映射"的结果对象。
然后通过 applyNestedResultMappings() 方法处理嵌套映射,在处理过程中,会从 ancestorObjects 集合中获取外层对象,并将嵌套映射产生的结果对象设置到外层对象的属性中。
处理完之后,就清理 ancestorObjects 集合,并将外层对象保存到 nestedResultObjects 集合中,等待后续的映射步骤继续使用。这里使用的 Key 就是前面创建的 CacheKey 对象。
了解了外层映射的核心步骤之后,下面我们一起来看一下 getRowValue() 方法的具体实现:
java
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
final String resultMapId = resultMap.getId();
Object rowValue = partialObject;
if (rowValue != null) { // 检测外层对象是否已经存在,如果存在,直接执行嵌套映射的逻辑
final MetaObject metaObject = configuration.newMetaObject(rowValue);
putAncestor(rowValue, resultMapId);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
} else { // 外层对象不存在,先生成外层映射的对象
// ResultLoaderMap与延迟加载相关
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建外层对象
rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 创建外层对象关联的MetaObject对象
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, true)) { // 自动映射
// 自动映射ResultMap中未明确映射的列
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 处理ResultMap中明确映射的列
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
// 将"部分构造"的外层对象添加到ancestorObjects集合中
putAncestor(rowValue, resultMapId);
// 处理嵌套映射,其中会从ancestorObjects集合中获取外层对象,并将嵌套映射的结果对象设置到外层对象的属性中
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
// 清理ancestorObjects集合,删除外层对象
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
// 将外层对象记录到nestedResultObjects集合中,等待后续使用
nestedResultObjects.put(combinedKey, rowValue);
}
}
return rowValue;
}
3. applyNestedResultMappings() 方法
通过对外层对象的处理我们可以知道,处理嵌套映射的核心在于 applyNestedResultMappings() 方法,其中会遍历 ResultMap 中的每个 ResultMapping 对象。
针对嵌套映射对应的 ResultMapping 对象进行特殊处理,其核心处理步骤如下。
- 确保 ResultMapping 对象的 nestedResultMapId 字段值不为空,该字段值保存了嵌套映射的 ResultMapId;同时还会检查 resultSet 字段是否为空,如果不为空,则是多结果集的映射,不是嵌套映射。
- 确定此次嵌套映射使用的 ResultMap 对象,这里依赖上一讲介绍的 resolveDiscriminatedResultMap() 方法。
- 处理循环引用的场景。如果存在循环引用的情况,则此次嵌套映射不会执行,直接重用已存在的嵌套对象即可。这里会先检查在 ancestorObjects 集合中是否已经存在嵌套对象,如果存在,就可以重用这个嵌套对象。
- 为嵌套对象创建 CacheKey。嵌套对象的 CacheKey 除了包含嵌套对象的信息,还会包含外层对象的 CacheKey 信息,这样才能得到一个全局唯一的 CacheKey 对象。
- 对外层对象的集合属性进行特殊处理。如果外层对象中用于记录当前嵌套对象的属性为 Collection 类型,且该属性未初始化,则这里会初始化该集合。
- 调用 getRowValue() 方法完成嵌套映射,得到嵌套对象。嵌套映射是支持嵌套多层的,这也就是产生 getRowValue() 方法递归的原因。
- 通过 linkObjects() 方法,将步骤 6 中映射得到的嵌套对象保存到外层对象的对应属性中,底层会依赖外层对象的 MetaObject 实现属性的设置。
延迟加载
MyBatis 中的"延迟加载"是指在查询数据库的时候,MyBatis 不会立即将完整的对象加载到服务内存中,而是在业务逻辑真正需要使用这个对象或使用到对象中某些属性的时候,才真正执行数据库查询操作,将完整的对象加载到内存中。
MyBatis 实现延迟加载的底层原理是动态代理 ,但并不是《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》中介绍的 JDK 动态代理,而是通过字节码生成方式实现的动态代理,底层依赖 cglib 和 javassit 两个库实现动态代码生成。
这里我们简单说明一下,之所以不用 JDK 动态代理是因为 JDK 动态代理在生成代理对象的时候,要求目标类必须实现接口,而通过 MyBatis 映射产生的结果对象基本都是 POJO 对象,没有实现任何接口,所以 JDK 动态代理不适用。
下面我们先简单了解一下 cglib 和 javassist 这两个库的基本使用,这样才能看懂 MyBatis 延迟加载的逻辑。
1. cglib
cglib 实现动态代理的底层原理是字节码生成技术,具体就是使用字节码生成技术生成一个目标类的子类,然后在这个子类中进行方法重写,并在重写的方法中进行拦截,实现代理对象的相关功能。
既然使用生成子类的方式来实现动态代理,那根据 Java 的语法规则,final 关键字修饰的方法无法被子类覆盖,自然也就无法通过 cglib 实现代理,所以我们可以将 cglib 与 JDK 动态代理作为互补的两个方案一起使用,在 Spring 等很多开源框架中,也都会同时使用这两个代理生成方式。
那如何使用 cglib 实现动态代理的功能呢?下面我们就来看看 cglib 的基础使用,在 cglib 中有一个关键的接口------ Callback 接口,它有很多子接口,如下图所示:

Callback 接口继承关系图
这里我们重点关注 MethodInterceptor 接口,它可以实现方法拦截的功能,可参考下面这个简单的实现:
java
public class CglibProxyDemo implements MethodInterceptor {
// cglib中的Enhancer对象
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
// 代理类的父类
enhancer.setSuperclass(clazz);
// 添加Callback对象
enhancer.setCallback(this);
// 通过cglib动态创建子类实例并返回
return enhancer.create();
}
// intercept()方法中实现了方法拦截
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("before operation...");
// 调用父类中的方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("after operation...");
return result;
}
}
下面我们创建一个目标类------ CglibMainDemo,这也是整个示例的入口类,这里使用 CglibProxyDemo 创建 CglibMainDemo 的代理对象,并执行 method() 方法:
java
public class CglibMainDemo { // 父类,也是代理的目标类
public String method(String str) { // 被代理的目标方法
System.out.println(str);
return "CglibMainDemo:" + str;
}
public static void main(String[] args) {
CglibProxyDemo proxy = new CglibProxyDemo();
// 获取CglibMainDemo的代理对象
CglibMainDemo proxyImp = (CglibMainDemo) proxy.getProxy(CglibMainDemo.class);
// 执行代理对象的method()方法
String result = proxyImp.method("test");
System.out.println(result);
}
}
执行 CglibMainDemo 的 main() 方法,我们可以看到控制台中,CglibMainDemo.method() 方法前后都出现了相应的拦截输出(即 "before operation" 和 "after operation"),这也就实现了代理的效果。
2. Javassist
Javassist 是一个操纵 Java 字节码的类库,我们可以直接通过 Javassist 提供的 Java API 动态生成或修改类结构。Javassist 提供的 Java API 非常多,这里我们重点来看如何使用 javassist 创建动态代理。
首先创建 JavassistDemo 类,其中提供了一个属性和一个方法,它是代理的目标类,通过 javassist 创建的代理类会继承 JavassistDemo,如下示例:
java
public class JavassistDemo {
private String demoProperty = "demo-value"; // 字段
// demoProperty字段对应的getter/setter方法
public String getDemoProperty() {
return demoProperty;
}
public void setDemoProperty(String demoProperty) {
this.demoProperty = demoProperty;
}
// JavassistDemo的成员方法
public void operation() {
System.out.println("operation():" + this.demoProperty);
}
}
javassist 本质上也是通过动态生成目标类的子类的方式实现动态代理的,下面我们就使用 javassist 库为 JavassistDemo 生成代理类,具体实现如下:
java
public class JavassitMainDemo {
public static void main(String[] args) throws Exception {
// 创建ProxyFactory工厂实例,它负责动态生成JavassistDemo的子类
ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(JavassistDemo.class);
// 设置Filter,用于确定哪些方法调用需要被代理
factory.setFilter(new MethodFilter() {
public boolean isHandled(Method m) {
if (m.getName().equals("operation")) {
return true;
}
return false;
}
});
// 设置拦截处理逻辑,被拦截的方法会执行MethodHandler中的逻辑
factory.setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed,
Object[] args) throws Throwable {
System.out.println("before operation");
Object result = proceed.invoke(self, args);
System.out.println("after operation");
return result;
}
});
// 生成代理类,并根据代理类创建代理对象
Class<?> c = factory.createClass();
JavassistDemo javassistDemo = (JavassistDemo) c.newInstance();
// 执行operation()方法时会被拦截,进而执行代理逻辑
javassistDemo.operation();
System.out.println(javassistDemo.getDemoProperty());
}
}
执行 JavassitMainDemo 的 main() 方法,我们可以看到控制台在 JavassistDemo.operation() 方法的输出前后,都添加了相应的拦截输出(即 "before operation" 和 "after operation"),这就是我们想要的代理效果。
3. 辅助类
了解了 cglib 和 javassist 的基本原理之后,我们接下来再介绍一下 MyBatis 中与延迟加载相关的辅助类。
首先来看 ResultLoader 辅助类,它记录了一次延迟加载涉及的全部信息,其中包括延迟执行的 SQL 语句(boundSql 字段)、Sql 的实参(parameterObject 字段)、用于执行延迟 SQL 的线程池(executor 字段)以及延迟加载的对象类型(targetType 字段)等,这些信息在真正执行加载操作的时候,都是必要的信息。
ResultLoader 中核心的方法是 loadResult() 方法,其中会先通过 selectList() 方法执行 boundSql 这条延迟加载的 SQL 语句,得到的是一个 List<Object>
集合。在 selectList() 方法中会使用到 Executor 来执行 SQL 语句,这部分的核心内容我们将在后面的课时中详细分析。
接下来通过 ResultExtractor 从这个 List 集合中提取到延迟加载的真正对象,这里就涉及了 List 集合向 targetType 转换的一些逻辑:
- 如果目标类型就是 List,那 ResultExtractor 无须进行任何转换,直接返回 List;
- 如果目标类型是 Collection 子类、数组类型,则 ResultExtractor 会创建一个元素为 targetType 类型的集合对象,并将 List
<Object>
集合中元素项复制到其中; - 如果目标类型是一个普通 Java 对象,且上面得到的 List 长度为 1,则从 List 中获取到唯一的元素,并转换成 targetType 类型的对象并返回。
在一个 ResultMap 中,我们可以配置多个延迟加载的属性,这些属性与对应的 ResultLoader 的映射关系就记录在一个 ResultLoaderMap 对象中,ResultLoaderMap 中的 loaderMap 字段(HashMap<String, LoadPair>
类型)就用来维护这一关系,LoadPair 对象就是用来维护 ResultLoader 对象以及一些配置信息的。
ResultLoaderMap 提供了一个 load(String) 方法,参数是触发加载的属性名称,在执行这个方法的时候,会从 loaderMap 中获取(并删除)指定属性对应的 ResultLoader 对象,并调用其 load() 方法执行延迟 SQL,完成延迟加载。这个方法是在 cglib 和 javassist 生成的代理对象中被调用的(如下图所示),从而实现在使用某个属性时触发延迟加载的效果。

ResultLoaderMap.load() 方法的调用点
ResultLoaderMap 中还有一个 loadAll() 方法,这个方法会触发 loaderMap 中全部 ResultLoader 的 load() 方法,将所有延迟加载的对象都加载上来。
4. 代理工厂
为了同时接入 cglib 和 javassist 两种生成动态代理的方式,MyBatis 提供了一个抽象的 ProxyFactory 接口来抽象动态生成代理类的基本行为,同时提供了下图中的两个实现类来接入上述两种生成方式:

ProxyFactory 的实现类图
ProxyFactory 接口中定义的核心方法是 createProxy() 方法,从名字也能看出这个方法是用来生成代理对象的。
在 JavassistProxyFactory 实现中,createProxy() 方法通过调用 EnhancedResultObjectProxyImpl 这个内部类的 createProxy() 方法来创建代理对象,具体实现与前文介绍的 JavassitMainDemo 类似,其中先是创建 javassist.util.proxy.ProxyFactory 对象,然后设置父类以及 MethodHandler 等信息,最后通过 javassist.util.proxy.ProxyFactory 的 create() 方法创建代理对象。
这里使用到 MethodHandler 实现就是 EnhancedResultObjectProxyImpl 本身,在其 invoke() 方法中首先会在 loaderMap 集合上加锁防止并发,然后通过 lazyLoader 集合的长度,判断是否存在延迟加载的属性。
在存在延迟加载属性的时候,会执行如下延迟加载操作。
- 首先,会优先检查全局的 aggressiveLazyLoading 配置和 lazyLoadTriggerMethods 配置。如果 aggressiveLazyLoading 配置为 true,或此次调用方法名称包含于 lazyLoadTriggerMethods 配置的方法名列表中,会立刻将该对象的全部延迟加载属性都加载上来,即触发 ResultLoaderMap.loadAll() 方法。
- 接下来,检查此次调用的方法是否为属性对应的 setter 方法,如果是,则该属性已经被赋值,无须再执行延迟加载操作,可以从 ResultLoaderMap 集合中删除该属性以及对应的 ResultLoader 对象。
- 最后,检测此次调用的方法是否为属性对应的 getter 方法,如果是,触发对应的 ResultLoader.load() 方法,完成延迟加载。
完成上述延迟加载操作之后,会释放 loaderMap 集合上的锁,然后调用目标对象的方法,完成真正的属性读写操作。
CglibProxyFactory 与 JavassistProxyFactory 的核心实现非常类似。CglibProxyFactory 中也定义了一个 EnhancedResultObjectProxyImpl 内部类,但是该内部类继承的是 cglib 中的 MethodHandler 接口,并通过 cglib 库的 API 实现代理逻辑。CglibProxyFactory 的具体实现,我就不赘述了,就留给你类比着分析了。
5. 延迟加载实现细节
了解了 MyBatis 中延迟加载的底层原理和相关辅助类,我们回到 DefaultResultSetHandler 中,看一下映射处理流程中与延迟加载相关的实现细节。
在 DefaultResultSetHandler.getPropertyMappingValue() 方法处理单个 ResultMapping 映射规则时候,会调用 getNestedQueryMappingValue() 方法处理嵌套映射,其中会有这么一段逻辑:
java
// 创建ResultLoader对象
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
// 根据是否延迟加载的配置决定value的值
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERRED;
} else {
value = resultLoader.loadResult();
}
我们可以清晰地看到,这里会检测该嵌套映射是否开启了延迟加载特性。如果开启了,则在 ResultLoaderMap 中记录延迟加载属性以及对应的 ResultLoader 对象,并返回 DEFERED 这个公共的占位符对象;如果未开启延迟加载特性,则直接执行嵌套查询,完成相应映射操作得到相应的结果对象。
另一个延迟加载的实现细节是在 createResultObject() 方法中,其中有如下代码片段:
java
for (ResultMapping propertyMapping : propertyMappings) {
// 检测所有ResultMapping规则,是否开启了延迟加载特性
if (propertyMapping.getNestedQueryId() != null &&
propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject,
lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
从上面这段代码中我们可以看到,如果检测到了延迟加载的属性,则会通过前面介绍的 ProxyFactory 为结果对象创建代理对象,然后在真正使用到延迟加载属性(即调用其 getter 方法)的时候,触发代理对象完成该属性的真正加载。
多结果集处理
在了解了简单映射、嵌套映射以及延迟加载的处理逻辑之后,下面我们再来介绍一下 MyBatis 中多结果集的处理逻辑。
在 getPropertyMappingValue() 方法中处理某个属性的映射时,有下面这个代码片段:
java
if (propertyMapping.getResultSet() != null) {
// 指定了resultSet属性,则等待后续结果集解析
addPendingChildRelation(rs, metaResultObject, propertyMapping);
return DEFERRED;
}
这段代码的含义是:这个属性的值来自后续的结果集(对应的结果集名称通过 resultSet 指定),后续结果集在这一时刻还未处理,所以会通过 addPendingChildRelation() 方法将该映射信息添加到 nextResultMaps 集合以及 pendingRelations 集合中暂存。
在 pendingRelations 集合中维护了 CacheKey 到 PendingRelation 对象之间的映射,PendingRelation 中维护了当前 ResultMapping 以及外层结果对象,nextResultMaps 集合中维护了 ResultSet 名称与当前 ResultMapping 对象的映射。
处理 nextResultMaps 集合的地方是在 handleResultSets() 方法中。在 handleResultSets() 方法完成全部 ResultMapping 映射之后,会开始遍历 nextResultMaps 集合,根据其中每个 ResultMapping 对象指定的 ResultMap 对后续的多个结果集进行映射,并将映射得到的结果对象设置到外层对象的相应属性中,相关的代码片段如下:
java
while (rsw != null && resultSetCount < resultSets.length) {
// 获取nextResultMaps中的ResultMapping对象
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
// 获取ResultMapping中指定的ResultMap映射规则
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
// 进行结果集映射,得到的结果对象会添加到外层结果对象的相应属性中
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt); // 继续获取下一个ResultSet
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
处理 pendingRelations 集合的地方是在 linkToParents() 方法中,该方法会从 pendingRelations 集合中获取结果对象所在外层对象,然后通过 linkObjects() 方法进行设置。
到此为止,MyBatis 中结果集映射的核心内容就介绍完了。
总结
紧接着上一讲的内容,我们继续介绍了 MyBatis 中关于结果集映射的相关知识点。
- 首先,重点讲解了 DefaultResultSetHandler 中嵌套映射的实现逻辑。
- 然后,介绍了 MyBatis 中延迟加载的实现细节,其中还详细说明了 MyBatis 实现延迟加载的两种方案以及 MyBatis 对这两种方案的封装和统一。
- 最后,简单分析了 MyBatis 对多结果集处理的实现。
除了上面介绍的这些核心映射方式之外,MyBatis 还支持游标、存储过程中的输出参数等方式返回查询结果,相关的逻辑也是在 DefaultResultSetHandler 中实现的,相关的方法就作为课后作业留给你自己分析了。
十九、Executor才是执行SQL语句的幕后推手(下)
这一讲,我们来介绍剩余的四个重点 Executor 实现。

Executor 接口继承关系图
SimpleExecutor
我们来看 BaseExecutor 的第一个子类------ SimpleExecutor,同时它也是 Executor 接口最简单的实现。
正如上一讲中分析的那样,BaseExecutor 通过模板方法模式实现了读写一级缓存、事务管理等不随场景变化的基础方法,在 SimpleExecutor、ReuseExecutor、BatchExecutor 等实现类中,不再处理这些不变的逻辑,而只要关注 4 个 do*() 方法的实现即可。
这里我们重点来看 SimpleExecutor 中 doQuery() 方法的实现逻辑。
- 通过 newStatementHandler() 方法创建 StatementHandler 对象,其中会根据 MappedStatement.statementType 配置创建相应的 StatementHandler 实现对象,并添加 RoutingStatementHandler 装饰器。
- 通过 prepareStatement() 方法初始化 Statement 对象,其中还依赖 ParameterHandler 填充 SQL 语句中的占位符。
- 通过 StatementHandler.query() 方法执行 SQL 语句,并通过我们前面[14]和[15]讲介绍的 DefaultResultSetHandler 将 ResultSet 映射成结果对象并返回。
doQuery() 方法的核心代码实现如下所示:
java
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建StatementHandler对象,实际返回的是RoutingStatementHandler对象(我们在第16讲介绍过)
// 其中根据MappedStatement.statementType选择具体的StatementHandler实现
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 完成StatementHandler的创建和初始化,该方法会调用StatementHandler.prepare()方法创建
// Statement对象,然后调用StatementHandler.parameterize()方法处理占位符
stmt = prepareStatement(handler, ms.getStatementLog());
// 调用StatementHandler.query()方法,执行SQL语句,并通过ResultSetHandler完成结果集的映射
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
SimpleExecutor 中的 doQueryCursor()、update() 等方法实现与 doQuery() 方法的实现基本类似,这里不再展开介绍,你若感兴趣的话可以参考源码进行分析。
ReuseExecutor
你如果有过 JDBC 优化经验的话,可能会知道重用 Statement 对象是一种常见的优化手段,主要目的是减少 SQL 预编译开销,同时还会降低 Statement 对象的创建和销毁频率,这在一定程度上可以提升系统性能。
ReuseExecutor 这个 BaseExecutor 实现就实现了重用 Statement 的优化,ReuseExecutor 维护了一个 statementMap 字段(HashMap类型)来缓存已有的 Statement 对象,该缓存的 Key 是 SQL 模板,Value 是 SQL 模板对应的 Statement 对象。这样在执行相同 SQL 模板时,我们就可以复用 Statement 对象了。
ReuseExecutor 中的 do*() 方法实现与前面介绍的 SimpleExecutor 实现完全一样,两者唯一的区别在于其中依赖的 prepareStatement() 方法:SimpleExecutor 每次都会创建全新的 Statement 对象,ReuseExecutor 则是先尝试查询 statementMap 缓存,如果缓存命中,则会重用其中的 Statement 对象。
另外,在事务提交/回滚以及 Executor 关闭的时候,需要同时关闭 statementMap 集合中缓存的全部 Statement 对象,这部分逻辑是在 doFlushStatements() 方法中实现的,核心代码如下:
java
public List<BatchResult> doFlushStatements(boolean isRollback) {
// 关闭statementMap集合中缓存的全部Statement对象
for (Statement stmt : statementMap.values()) {
closeStatement(stmt);
}
// 清空statementMap集合
statementMap.clear();
return Collections.emptyList();
}
BatchExecutor
批处理是 JDBC 编程中的另一种优化手段。
JDBC 在执行 SQL 语句时,会将 SQL 语句以及实参通过网络请求的方式发送到数据库,一次执行一条 SQL 语句,一方面会减小请求包的有效负载,另一个方面会增加耗费在网络通信上的时间。通过批处理的方式,我们就可以在 JDBC 客户端缓存多条 SQL 语句,然后在 flush 或缓存满的时候,将多条 SQL 语句打包发送到数据库执行,这样就可以有效地降低上述两方面的损耗,从而提高系统性能。
不过,有一点需要特别注意:每次向数据库发送的 SQL 语句的条数是有上限的,如果批量执行的时候超过这个上限值,数据库就会抛出异常,拒绝执行这一批 SQL 语句,所以我们需要控制批量发送 SQL 语句的条数和频率。
BatchExecutor 是用于实现批处理的 Executor 实现 ,其中维护了一个 List<Statement>
集合(statementList 字段)用来缓存一批 SQL,每个 Statement 可以写入多条 SQL。
我们知道 JDBC 的批处理操作只支持 insert、update、delete 等修改操作,也就是说 BatchExecutor 对批处理的实现集中在 doUpdate() 方法中。在 doUpdate() 方法中追加一条待执行的 SQL 语句时,BatchExecutor 会先将该条 SQL 语句与最近一次追加的 SQL 语句进行比较,如果相同,则追加到最近一次使用的 Statement 对象中;如果不同,则追加到一个全新的 Statement 对象,同时会将新建的 Statement 对象放入 statementList 缓存中。
下面是 BatchExecutor.doUpdate() 方法的核心逻辑:
java
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
// 创建StatementHandler对象
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
// 获取此次追加的SQL模板
final String sql = boundSql.getSql();
final Statement stmt;
// 比较此次追加的SQL模板与最近一次追加的SQL模板,以及两个MappedStatement对象
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
// 两者相同,则获取statementList集合中最后一个Statement对象
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt); // 设置实参
// 查找该Statement对象对应的BatchResult对象,并记录用户传入的实参
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
// 创建新的Statement对象
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);// 设置实参
// 更新currentSql和currentStatement
currentSql = sql;
currentStatement = ms;
// 将新创建的Statement对象添加到statementList集合中
statementList.add(stmt);
// 为新Statement对象添加新的BatchResult对象
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
这里使用到的 BatchResult 用于记录批处理的结果,一个 BatchResult 对象与一个 Statement 对象对应,BatchResult 中维护了一个 updateCounts 字段(int[] 数组类型)来记录关联 Statement 对象执行批处理的结果。
添加完待执行的 SQL 语句之后,我们再来看一下 doFlushStatements() 方法,其中会通过 Statement.executeBatch() 方法批量执行 SQL,然后 SQL 语句影响行数以及数据库生成的主键填充到相应的 BatchResult 对象中返回。下面是其核心实现:
java
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
// 用于储存批处理的结果
List<BatchResult> results = new ArrayList<>();
// 如果明确指定了要回滚事务,则直接返回空集合,忽略statementList集合中记录的SQL语句
if (isRollback) {
return Collections.emptyList();
}
for (int i = 0, n = statementList.size(); i < n; i++) { // 遍历statementList集合
Statement stmt = statementList.get(i);// 获取Statement对象
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i); // 获取对应BatchResult对象
try {
// 调用Statement.executeBatch()方法批量执行其中记录的SQL语句,并使用返回的int数组
// 更新BatchResult.updateCounts字段,其中每一个元素都表示一条SQL语句影响的记录条数
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
// 获取配置的KeyGenerator对象
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
// 获取数据库生成的主键,并记录到实参中对应的字段
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) {
// 其他类型的KeyGenerator,会调用其processAfter()方法
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
closeStatement(stmt);
} catch (BatchUpdateException e) {
// 异常处理逻辑
}
// 添加BatchResult到results集合
results.add(batchResult);
}
return results;
} finally {
// 释放资源
}
}
CachingExecutor
CachingExecutor 是我们最后一个要介绍的 Executor 接口实现类,它是一个 Executor 装饰器实现,会在其他 Executor 的基础之上添加二级缓存的相关功能。在上一讲中,我们已经介绍过了一级缓存,下面就接着讲解二级缓存相关的内容。
1. 二级缓存
我们知道一级缓存的生命周期默认与 SqlSession 相同,而这里介绍的 MyBatis 中的二级缓存则与应用程序的生命周期相同。与二级缓存相关的配置主要有下面三项。
第一项,二级缓存全局开关。这个全局开关是 mybatis-config.xml 配置文件中的 cacheEnabled 配置项。当 cacheEnabled 被设置为 true 时,才会开启二级缓存功能,开启二级缓存功能之后,下面两项的配置才会控制二级缓存的行为。
第二项,命名空间级别开关 。在 Mapper 配置文件中,可以通过配置 <cache>
标签或 <cache-ref>
标签开启二级缓存功能。
- 在解析到
<cache>
标签时,MyBatis 会为当前 Mapper.xml 文件对应的命名空间创建一个关联的 Cache 对象(默认为 PerpetualCache 类型的对象),作为其二级缓存的实现。此外,<cache>
标签中还提供了一个 type 属性,我们可以通过该属性使用自定义的 Cache 类型。 - 在解析到
<cache-ref>
标签时,MyBatis 并不会创建新的 Cache 对象,而是根据<cache-ref>
标签的 namespace 属性查找指定命名空间对应的 Cache 对象,然后让当前命名空间与指定命名空间共享同一个 Cache 对象。
第三项,语句级别开关 。我们可以通过 <select>
标签中的 useCache 属性,控制该 select 语句查询到的结果对象是否保存到二级缓存中,useCache 属性默认值为 true。
2. TransactionalCache
了解了二级缓存的生命周期、基本概念以及相关配置之后,我们开始介绍 CachingExecutor 依赖的底层组件。
CachingExecutor 底层除了依赖 PerpetualCache 实现来缓存数据之外,还会依赖 TransactionalCache 和 TransactionalCacheManager 两个组件,下面我们就一一详细介绍下。
TransactionalCache 是 Cache 接口众多实现之一,它也是一个装饰器,用来记录一个事务中添加到二级缓存中的缓存。
TransactionalCache 中的 entriesToAddOnCommit 字段(Map<Object, Object>
类型)用来暂存当前事务中添加到二级缓存中的数据,这些数据在事务提交时才会真正添加到底层的 Cache 对象(也就是二级缓存)中。这一点我们可以从 TransactionalCache 的 putObject() 方法以及 flushPendingEntries() 方法(commit() 方法会调用该方法)中看到相关代码实现:
java
public void putObject(Object key, Object object) {
// 将数据暂存到entriesToAddOnCommit集合
entriesToAddOnCommit.put(key, object);
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 将entriesToAddOnCommit集合中的数据添加到二级缓存
delegate.putObject(entry.getKey(), entry.getValue());
}
... // 其他逻辑
}
那为什么要在事务提交时才将 entriesToAddOnCommit 集合中的缓存数据写入底层真正的二级缓存中,而不是像操作一级缓存那样,每次查询都直接写入缓存呢?其实这是为了防止出现"脏读"。
我们假设当前数据库的隔离级别是"不可重复读",如下图所示,两个业务线程分别开启了 T1、T2 两个事务:
- 在事务 T1 中添加了记录 A,之后查询记录 A;
- 事务 T2 会查询记录 A。

两事务并发操作的示意图
如果事务 T1 查询记录 A 时,就将 A 对应的结果对象写入二级缓存,那在事务 T2 查询记录 A 时,会从二级缓存中直接拿到结果对象。此时的事务 T1 仍然未提交,也就出现了"脏读"。
我们按照 TransactionalCache 的实现再来分析下,事务 T1 查询 A 数据的时候,未命中二级缓存,就会击穿到数据库,因为写入和读取 A 都是在事务 T1 中,所以能够查询成功,同时更新 entriesToAddOnCommit 集合。事务 T2 查询记录 A 时,同样也会击穿二级缓存,访问数据库,因为此时写入和读取 A 是不同的事务,且数据库的事务隔离级别为"不可重复读",这就导致事务 T2 无法查询到记录 A,也就避免了"脏读"。
如上图所示,事务 T1 在提交时,会将 entriesToAddOnCommit 中的数据添加到二级缓存中,所以事务 T2 第二次查询记录 A 时,会命中二级缓存,也就出现了同一事务中多次读取的结果不同的现象,也就是我们说的"不可重复读"。
TransactionalCache 中的另一个核心字段是 entriesMissedInCache,它用来记录未命中的 CacheKey 对象。在 getObject() 方法中,我们可以看到写入 entriesMissedInCache 集合的相关代码片段:
java
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
... // 其他逻辑
}
在事务提交的时候,会将 entriesMissedInCache 集合中的 CacheKey 写入底层的二级缓存(写入时的 Value 为 null)。在事务回滚时,会调用底层二级缓存的 removeObject() 方法,删除 entriesMissedInCache 集合中 CacheKey。
你可能会问,为什么要用 entriesMissedInCache 集合记录未命中缓存的 CacheKey 呢?为什么还要在缓存结束时处理这些 CacheKey 呢?这主要是与[第 9 讲]介绍的 BlockingCache 装饰器相关。在前面介绍 Cache 时我们提到过,CacheBuilder 默认会添加 BlockingCache 这个装饰器,而 BlockingCache 的 getObject() 方法会有给 CacheKey 加锁的逻辑,需要在 putObject() 方法或 removeObject() 方法中解锁,否则这个 CacheKey 会被一直锁住,无法使用。
看完 TransactionalCache 的核心实现之后,我们再来看 TransactionalCache 的管理者------ TransactionalCacheManager,其中定义了一个 transactionalCaches 字段(HashMap类型)维护当前 CachingExecutor 使用到的二级缓存,该集合的 Key 是二级缓存对象,Value 是装饰二级缓存的 TransactionalCache 对象。
TransactionalCacheManager 中的方法实现都比较简单,都是基于 transactionalCaches 集合以及 TransactionalCache 的同名方法实现的,这里不再展开介绍,你若感兴趣的话可以参考源码进行分析。
3. 核心实现
了解了二级缓存基本概念以及 TransactionalCache 核心实现之后,我们再来看 CachingExecutor 的核心实现。
CachingExecutor 作为一个装饰器,其中自然会维护一个 Executor 类型字段指向被装饰的 Executor 对象,同时它还创建了一个 TransactionalCacheManager 对象来管理使用到的二级缓存。
CachingExecutor 的核心在于 query() 方法,其核心操作大致可总结为如下。
- 获取 BoundSql 对象,创建查询语句对应的 CacheKey 对象。
- 尝试获取当前命名空间使用的二级缓存,如果没有指定二级缓存,则表示未开启二级缓存功能。如果未开启二级缓存功能,则直接使用被装饰的 Executor 对象进行数据库查询操作。如果开启了二级缓存功能,则继续后面的步骤。
- 查询二级缓存,这里使用到 TransactionalCacheManager.getObject() 方法,如果二级缓存命中,则直接将该结果对象返回。
- 如果二级缓存未命中,则通过被装饰的 Executor 对象进行查询。正如前面介绍的那样,BaseExecutor 会先查询一级缓存,如果一级缓存未命中时,才会真正查询数据库。最后,会将查询到的结果对象放入 TransactionalCache.entriesToAddOnCommit 集合中暂存,等待事务提交时再写入二级缓存。
下面是 CachingExecutor.query() 方法的核心代码片段:
java
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建相应的CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用下面的query()方法重载
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache(); // 获取该命名空间使用的二级缓存
if (cache != null) { // 是否开启了二级缓存功能
flushCacheIfRequired(ms); // 根据<select>标签配置决定是否需要清空二级缓存
// 检测useCache配置以及是否使用了resultHandler配置
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql); // 是否包含输出参数
// 查询二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 二级缓存未命中,通过被装饰的Executor对象查询结果对象
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将查询结果放入TransactionalCache.entriesToAddOnCommit集合中暂存
tcm.putObject(cache, key, list);
}
return list;
}
}
// 如果未开启二级缓存,直接通过被装饰的Executor对象查询结果对象
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
总结
紧接上一讲的内容,我们详细分析了 Executor 接口的核心实现类。
- 首先介绍了最常用、也是最简单的 Executor 实现类------ SimpleExecutor 实现,它底层完全依赖 StatementHandler、DefaultResultSetHandler 和 JDBC API 完成数据库查询和结果集映射。
- 接下来讲解了 ReuseExecutor 和 BatchExecutor 实现,其中 ReuseExecutor 实现了 Statement 对象的重用,而 BatchExecutor 实现了批处理的相关逻辑。
- 最后讲解了 CachingExecutor 实现,其中重点介绍了二级缓存的内容以及 CachingExecutor 底层的 TransactionalCache、TransactionalCacheManager 等核心组件。