17 Executor 才是执行 SQL 语句的幕后推手(上)
在上一讲中,我们介绍了 MyBatis 中结果集映射的核心逻辑位于 DefaultResultSetHandler 之中,然后深入分析了 DefaultResultSetHandler 与简单结果集映射相关的核心实现,这是 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。
- 检测
- 通过 getRowValue() 方法完成当前记录行的映射,得到最终的结果对象,其中还会将结果对象添加到 nestedResultObjects 集合中。
- 通过 storeObject() 方法将生成的结果对象保存到 ResultHandler 中。
在上述过程中,有很多步骤的实现已经在上一讲的简单映射部分介绍过了,例如,前三步中使用到的 skipRows()、shouldProcessMoreRows() 和 resolveDiscriminatedResultMap() 三个方法。所以,下面我们就从(第 4 步)创建 CacheKey 开始介绍。
1. 创建 CacheKey
创建 CacheKey 的核心逻辑在 createRowKey() 方法中,该方法构建 CacheKey 的过程是这样的:尝试使用
标签或
标签中定义的列名以及对应列值组成 CacheKey 对象;没有定义
标签或
标签,则由 ResultMap 中映射的列名和对应列值一起构成 CacheKey 对象;这样如果依然无法创建 CacheKey 的话,就由 ResultSet 中所有列名以及对应列值一起构成 CacheKey 对象。
无论是使用
、
指定的列名和列值来创建 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() 方法的具体实现:
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 接口,它有很多子接口,如下图所示:
![图片9.png](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e5%89%96%e6%9e%90%20MyBatis%20%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86-%e5%ae%8c/assets/Cgp9HWBLPxOAVTGAAADxvYPW-EI795.png)
Callback 接口继承关系图
这里我们重点关注 MethodInterceptor 接口,它可以实现方法拦截的功能,可参考下面这个简单的实现:
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() 方法:
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,如下示例:
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 生成代理类,具体实现如下:
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