Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler
一丶前情回顾
书接上回,下面是SimpleExecutor执行查询的主要逻辑

prepareStatement

实现获取数据库连接,

其中连接是从Transaction.getConnection方法,Transaction存在一个实现SpringManagedTransaction(如何整合了Spring的话)这个后续研究spring 和mybatis整合的时候在详细看看
根据连接准备Statement

instantiateStatement
根据数据库连接,创建Statement,具体什么类型的Statement取决于当前用的什么StatementHandler
setStatementTimeout

这以可以看到,超时时长的有先见,Mapper.xml上面的超时时长>全局配置的默认超时时长
给占位符设置参数
这部分使用DefaultParameterHandler的setParameters 方法

这里根据指定的TypeHandler 调用setParameter方法 去设置参数

至此我们以及将参数设置到PreparedStatement 上了
相当于
select * from A where a=#{a}
这里的a=?已经成功设置上去了
二丶执行查询
这些查询交由StatementHandler处理,可以看下图,
对于PreparedStatement就是调用其execute方法

对于CallableStatement 多了一个处理处理handleOutputParameters这部分应该和存储过程相关

其他的Statement

获取sql 执行
还有个RoutingStatementHandler,就是调用被装饰着的query方法
三丶处理结果集
终于到了这篇总结的重点,处理结果集,上面图可以看到Statement执行excute后,使用ResultSetHandler处理结果集,ResultSetHandler只有一个实现类 DefaultResultSetHandler
1.处理多结果集
存储过程存在多结果集的情况,

2.处理结果集


不存在嵌套子查询的时候,使用handleRowValuesForSimpleResultMap

这里出现一个类DefaultResultContext,实现了ResultContext,这是结果上下文,主要的职责是控制处理结果行的停止,配合rowBounds实现内存分页,后面的storeObject就是将一行对应的对象存在list(似乎对map这种出惨有特殊处理,对于嵌套子查询也有特殊处理)

这里的自动映射应该是处理,没有指定resultMap 凭借对象属性和数据库列名进行映射的情况 ,后面applyPropertyMappings 处理指定resultMap 中column和 property的情况
例如我们执行下面test这条sql,
<resultMap id="typeHandlerTest" type="com.cuzz.domain.User">
<result column="eList" property="eList"
typeHandler="com.cuzz.mybatisInterceptor.ListVarcharTypeHandler"/>
</resultMap>
<select id="test" resultMap="typeHandlerTest">
SELECT *
FROM user
WHERE id = #{id}
</select>
User对象是

对于id和name 会执行 applyAutomaticMappings赋值对应属性,对于eList执行applyPropertyMappings赋值到属性

四丶mybatis插件实现原理
1.拦截器接口
public interface Interceptor {
//拦截 Invocation :当前被拦截对象 参数 和被拦截方法
Object intercept(Invocation invocation) throws Throwable;
//动态代理
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP 可以在这里给拦截器赋值一些属性
}
}
2.Plugin.wrap(target, this)方法如何实现拦截
- Plugin 实现了InvocationHandler——基于JDK动态代理


- 读取注解信息,获取当前拦截器要拦截什么类的什么方法

注意获取方法的方式
Method method = sig.type().getMethod(sig.method(), sig.args());
getMethod方法是没有办法获取到私有方法的,所有无法拦截一个私有方法
- 获取被代理类的接口

- 动态代理对象生成


3.动态代理生成的对象是怎么被使用的
如上我们知道了mybatis 是怎么支持插件的,根据拦截器上的信息生成动态代理对象,动态代理对象在执行方法的时候会进入拦截器的intercept拦截方法,那么动态代理的生成的对象在哪里被使用到昵
- Configuration 类 也就是mybatis的大管家,在new一些mybatis四大对象的时候会使用到插件



也就是说mybatis 只支持拦截ParmeterHandler,ResultSetHandler,StatementHandler,Excutor这四种对象
在mybatis执行中的使用的四大对象其实是被动态代理后的对象
五丶基于mybatis插件实现参数化类型TypeHandler
1.需求
存在实体
@Data
public class User {
@NotNull(message = "id")
private Integer id;
@NotNull(message = "name")
private String name; private List<E> eList; @Data
@AllArgsConstructor
@NoArgsConstructor
static class E {
Integer i;
String j;
}
}
存在表结构

我希望在插入User对象到数据表的时候,自动到eList转换为Json格式存入
查询User对象返回的时候,自动从Json格式字符串转换为
List<E>类型
2.实现TypeHandler
阅读上面结果集处理源码后,我们知道对一个字段的处理最终是使用了对应TypeHanlder进行读取,我们实现一个我们的TypeHandler实现Json的序列化和反序列化
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({List.class})
public class ParameterizeType2JsonStrTypeHandler extends BaseTypeHandler<Object> {
//设置参数 也就是eList插入的时候怎么设置到对应的PrepareStatement占位符上
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
//直接序列化成字符串
ps.setString(i, JSON.toJSONString(parameter));
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
//怎么根据字段名,返回属性值
//比如我们这里是List<E>,不能简单的序列化成List.class ,因为这样List内部其实存储的是JsonObject对象,没办法泛型擦除是这样的
//那么怎么拿到参数化类型昵 我们要的是List<确切的类型>
}
}
这里的getNullableResult 入参只有结果集 和列名,这显然不足以让我们拿到List<确切的类型>
3.“拦截”ResultSetHandler
我们知道处理一行数据到对象映射使用的方法是ResultSetHandler的handleRowValues方法
加上上面实现TypeHandler遇到的问题,看来我们需要实现一个自己的ResultSetHandler,在handleRowValues执行之前把当前处理对象的class 存储到一个位置,然后我们实现的TypeHandler在getNullableResult 执行的时候拿到class 根据字段名词找到对应属性,然后获取到数据的通用类型,然后再进行Json的反序列化
如何存储结果集转换目标对象类型的class
这里我们使用ThreadLocal进行存储ResultMap 可以根据 ResultMap中的方法拿到当前的类型
public class ResultSetProcessTypeMemory { private final static ThreadLocal<ResultMap> MEMORY = new ThreadLocal<>(); public static ResultMap get() {
return MEMORY.get();
} public static void set(ResultMap resultMap) {
MEMORY.set(resultMap);
} public static void remove() {
MEMORY.remove();
} }
拦截ResultSetHandler让它在 执行handleRowValues 之前把ResultMap 塞到ThreadLocal,处理完后删除ResultMap
按照我们的思路我们要拦截handleRowValues 方法,但是这个方法是私有方法,mybatis不支持拦截,我们在看看上层调用的位置——handleResultSets方法

这时候我们发现mybatis 为了实现存储过程多结果集的处理,使用循环处理每一个结果集的映射,我们可以拦截这个方法,在循环中向ThreadLocal塞ResultMap,但是代码侵入性有点太大了
实现自定义ResultSetHandler
上面我们企图拦截,发现可行但是不太好,我们看看 ResultSetHandler使用的地方,在StatementHandler实现类的 query方法,最后调用ResultHandler处理结果集

我们能不能自己实现一个ResultSetHandler 复写handleResultSets方法, 让它在 执行handleRowValues 之前把ResultMap 塞到ThreadLocal,处理完后删除ResultMap
public class MyResultSetHandler extends DefaultResultSetHandler { public MyResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, ResultHandler<?> resultHandler, BoundSql boundSql, RowBounds rowBounds) {
super(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
} @Override
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
//放入ThreadLocal
ResultSetProcessTypeMemory.set(resultMap);
super.handleRowValues(rsw, resultMap, resultHandler, rowBounds, parentMapping);
//移除
ResultSetProcessTypeMemory.remove();
}
}
注意这里的构造方法,DefaultResultSetHandler 存在·许多成员变量,后续handleRowValues方法会使用到这些成员变量,所有我们需要把这些属性设置上去,属性来自于哪儿昵

如上是BaseStatementHandler构造的时候 可以看到最后一行 创建resultHandler 传入的属性和 上面赋值私有属性的值是一样的,也就是说我们自定义ResultSetHandler构造方法入参需要的对象,可以在BaseStatementHandler中拿到,但是存在一个例外——RoutingStatementHandler,这个使用了装饰器模式其内部持有一个一个StatmentHandler对象,所有我们有了如下代码,实例化一个我们自己的ResultSetHandler(大体思路是反射拿属性)
private static MyResultSetHandler create(RoutingStatementHandler routingStatementHandler) {
BaseStatementHandler delegate = (BaseStatementHandler) ReflectUtil.getFieldValue(routingStatementHandler, "delegate");
return create(delegate);
} private static MyResultSetHandler create(BaseStatementHandler base) {
Executor executor = (Executor) ReflectUtil.getFieldValue(base, "executor");
MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(base, "mappedStatement");
ParameterHandler parameterHandler = (ParameterHandler) ReflectUtil.getFieldValue(base, "parameterHandler");
ResultHandler<?> resultHandler = (ResultHandler<?>) ReflectUtil.getFieldValue(base, "resultHandler");
BoundSql boundSql = (BoundSql) ReflectUtil.getFieldValue(base, "boundSql");
RowBounds rowBounds = (RowBounds) ReflectUtil.getFieldValue(base, "rowBounds");
return new MyResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
} public static MyResultSetHandler create(StatementHandler statementHandler) {
if (statementHandler instanceof BaseStatementHandler){
return create((BaseStatementHandler)statementHandler);
}
if (statementHandler instanceof RoutingStatementHandler){
return create((RoutingStatementHandler)statementHandler);
}
throw new UnsupportedOperationException();
}
4.拦截StatementHandler
上面的一通操作,我们搞出来一个自己的ResultSetHandler 但是怎么让mybatis 用我们的ResultSetHandler昵,我们可以拦截StatementHandler的query方法

@Intercepts({
@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class}
)
})
public class ListVarcharTypeHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
PreparedStatement ps = (PreparedStatement) args[0];
ps.execute();
StatementHandler target = (StatementHandler) invocation.getTarget();
//最后一步用我们自己的ResultSetHandler 处理
return MyResultSetHandler.create(target).handleResultSets(ps);
}
}
5.注入拦截器
/**
* mybatis配置
*/
@Configuration
public class MybatisConfiguration {
/**
* 注册拦截器
*/
@Bean
public ListVarcharTypeHandlerInterceptor mybatisInterceptor() {
return new ListVarcharTypeHandlerInterceptor();
}
}
6.完善TypeHandler
实现完第五步,我们可以从ThreadLocal中拿到ResultMap,接下来就是如何利用Json反序列化到属性上了
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({List.class})
public class ParameterizeType2JsonStrTypeHandler extends BaseTypeHandler<Object> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JSON.toJSONString(parameter));
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
//从TreadLocal拿
ResultMap resultMap = ResultSetProcessTypeMemory.get();
//拿到封装目标类型class
Class<?> type = resultMap.getType();
//根据字段名词 拿到field 这里可以优化下,忽略大小写吧
Field field = ReflectUtil.getField(type, columnName);
//拿到通用类型 可以拿到List<真实类型>
Type genericType = field.getGenericType();
//从resutSet拿到字符串内容,并且反序列化回去
return JSON.parseObject(rs.getString(columnName), genericType);
}
}
7.简单的一个测试
<resultMap id="typeHandlerTest" type="com.cuzz.domain.User">
<result column="eList" property="eList"
typeHandler="com.cuzz.mybatisInterceptor.ParameterizeType2JsonStrTypeHandler"/>//指定用我们的参数化类型TypeHandler
</resultMap>
<select id="test" resultMap="typeHandlerTest">
SELECT *
FROM user
WHERE id = #{id}
</select>
最后成功反序列化且类型没有错误
Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler的更多相关文章
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- mybatis(五):源码分析 - 结果集映射流程
- Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)
补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ...
- 手把手带你阅读Mybatis源码(三)缓存篇
前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...
- MyBatis 源码分析-项目总览
MyBatis 源码分析-项目总览 1.概述 本文主要大致介绍一下MyBatis的项目结构.引用参考资料<MyBatis技术内幕> 此外,https://mybatis.org/mybat ...
- Mybatis源码解析优秀博文
最近阅读了许久的mybatis源码,小有所悟.同时也发现网上有许多优秀的mybatis源码讲解博文.本人打算把自己阅读过的.觉得不错的一些博文列出来.以此进一步加深对mybatis框架的理解.其实还有 ...
- mybatis源码分析之06二级缓存
上一篇整合redis框架作为mybatis的二级缓存, 该篇从源码角度去分析mybatis是如何做到的. 通过上一篇文章知道,整合redis时需要在FemaleMapper.xml中添加如下配置 &l ...
- MyBatis 源码篇-SQL 执行的流程
本章通过一个简单的例子,来了解 MyBatis 执行一条 SQL 语句的大致过程是怎样的. 案例代码如下所示: public class MybatisTest { @Test public void ...
- MyBatis源码分析-MyBatis初始化流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- MyBatis源码分析-SQL语句执行的完整流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
随机推荐
- bash: accelerate: command not found
py AIGC Stable Diffusion文生图Lora模型微调实现虚拟上装详情 实验手册 实验报告 3. 安装Diffusers 进入PAI-DSW开发环境. 登录PAI控制台. ...
- 是因为不同的浏览器内核吗--Could not register service workers到底是怎么回事
什么是浏览器内核 浏览器内核(Rendering Engine),是浏览器最核心的部分. 它负责处理网页的HTML.CSS.JavaScript等代码,并将其转化为可视化的网页内容.即我们常说的对网页 ...
- 每天5分钟复习OpenStack(三)
每天5分钟复习OpenStack(三) 为什么要拉起kvm 虚拟机要熟悉这些操作? 作为一个运维工程师,将来有大量的时间是在制作镜像,镜像的制作就是在kvm虚拟化环境拉起kvm 管理的虚拟机的过程,安 ...
- Redis中的缓存雪崩、缓存击穿、缓存穿透问题
1. 什么是缓存雪崩 当我们提到缓存系统中的问题,缓存雪崩是一个经常被讨论的话题.缓存雪崩是指在某一时刻发生大量的缓存失效,导致瞬间大量的请求直接打到了数据库,可能会导致数据库瞬间压力过大甚至宕机.尤 ...
- APP攻防--反模拟器&反代理&反证书&真机逃逸&XP框架&Frida技术
APP攻防--反模拟器&反代理&反证书&真机逃逸&XP框架&Frida技术 APP抓包技术 关于APP抓包,使用burpsuite抓模拟器中的数据包,需要将模拟 ...
- 虹科分享 | HPC调度解决方案:HK-Adaptive在数字卫星图像领域的应用
2011年3月11日,日本海岸附近发生了9.0级地震.这次地震引发了强大的海啸,并向内陆传播了6英里,不仅使地球的轴心偏移了大约10到25厘米,还导致福岛核电站发生核紧急情况. 为了减少这场灾害的损失 ...
- Java替换RequestBody和RequestParam参数的属性
Java替换RequstBody和RequestParam参数的属性 本文主要讲解在Java环境中如何替换RequestBody和RequestParam参数中的属性 背景 近期由于接手的老项目中存在 ...
- angular,vue,react三大框架选型
三大框架,本质都是基于js的web应用(前端做的都是web应用包括移动)框架,他们都是帮助我们解决问题的工具,具体用哪个,要结合具体场景. 这三者中,Angular的适用领域相对窄一些,React可以 ...
- VS2022修改cs文件模板
在路径:C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\ItemTemplates\AspNetCore\Co ...
- BAPI_ACC_DOCUMENT_POST 解决多行一次性供应商凭证导入问题
POST 函数将一次性供应商 的信息放在抬头入参上,业务需要多个一次性供应商一起做凭证时,就满足不了. 抬头入参会把所有行的一次性给一样的名称. 想起之前做的 IF_EX_ACC_DOCUMENT~C ...