一丶前情回顾

书接上回,下面是SimpleExecutor执行查询的主要逻辑

  • prepareStatement

    1. 实现获取数据库连接,

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

    2. 根据连接准备Statement

      • instantiateStatement

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

      • setStatementTimeout

      这以可以看到,超时时长的有先见,Mapper.xml上面的超时时长>全局配置的默认超时时长

    3. 给占位符设置参数

      这部分使用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的反序列化

  1. 如何存储结果集转换目标对象类型的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();
    } }
  2. 拦截ResultSetHandler让它在 执行handleRowValues 之前把ResultMap 塞到ThreadLocal,处理完后删除ResultMap

    按照我们的思路我们要拦截handleRowValues 方法,但是这个方法是私有方法,mybatis不支持拦截,我们在看看上层调用的位置——handleResultSets方法

    这时候我们发现mybatis 为了实现存储过程多结果集的处理,使用循环处理每一个结果集的映射,我们可以拦截这个方法,在循环中向ThreadLocal塞ResultMap,但是代码侵入性有点太大了

  3. 实现自定义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的更多相关文章

  1. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  2. mybatis(五):源码分析 - 结果集映射流程

  3. Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)

    补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ...

  4. 手把手带你阅读Mybatis源码(三)缓存篇

    前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...

  5. MyBatis 源码分析-项目总览

    MyBatis 源码分析-项目总览 1.概述 本文主要大致介绍一下MyBatis的项目结构.引用参考资料<MyBatis技术内幕> 此外,https://mybatis.org/mybat ...

  6. Mybatis源码解析优秀博文

    最近阅读了许久的mybatis源码,小有所悟.同时也发现网上有许多优秀的mybatis源码讲解博文.本人打算把自己阅读过的.觉得不错的一些博文列出来.以此进一步加深对mybatis框架的理解.其实还有 ...

  7. mybatis源码分析之06二级缓存

    上一篇整合redis框架作为mybatis的二级缓存, 该篇从源码角度去分析mybatis是如何做到的. 通过上一篇文章知道,整合redis时需要在FemaleMapper.xml中添加如下配置 &l ...

  8. MyBatis 源码篇-SQL 执行的流程

    本章通过一个简单的例子,来了解 MyBatis 执行一条 SQL 语句的大致过程是怎样的. 案例代码如下所示: public class MybatisTest { @Test public void ...

  9. MyBatis源码分析-MyBatis初始化流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

  10. MyBatis源码分析-SQL语句执行的完整流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

随机推荐

  1. Writing your first Django app, part 1

    Let's learn by example. Throughout this tutorial, we'll walk you through the creation of a basic pol ...

  2. Java 魔法值处理的四种方法

    Java 魔法值处理方案 魔法值的定义 方法一 静态常量(不推荐) 方法二 接口中定义 方法三 定义在实体类 方法四 使用枚举类 enum 总结 魔法值的定义 魔法值是Java中突兀出现在代码中的常量 ...

  3. Mybatis_plus笔记

    Mybatis_plus笔记 在使用mybatis_plus的过程中我们可以明显的感受到他的强大之处.它就像是Mybatis和Jpa的结合体一样,它拥有jpa对单表的各种CRUD操作以及强大的条件构造 ...

  4. 【XXE漏洞】原理及实践演示

    一.原理 XML是用于传输和存储数据的一种格式,相当于一种信息传输工具,其中包含了XML声明,DTD文档类型定义.文档元素. XXE是xml外部实体注入漏洞,发生在应用程序解析XML输入时,没有禁止外 ...

  5. CSP2023 模拟赛总结合集

    9.9 ZZFLS 感觉 ucup 剩下的题完全不可做了啊!先对比赛时间来写总结对队友道歉(鞠躬.jpg 开题策略很失败.开场 30min 得的分数是一整场考试的分数. 开题,发现 T1 是水题,30 ...

  6. mybatis plus很好,但是我被它坑了!

    作者今天在开发一个后台发送消息的功能时,由于需要给多个用户发送消息,于是使用了 mybatis plus 提供的 saveBatch() 方法,在测试环境测试通过上预发布后,测试反应发送消息接口很慢得 ...

  7. 手撕Vuex-实现mutations方法

    经过上一篇章介绍,完成了实现 getters 的功能,那么接下来本篇将会实现 mutations 的功能. 在实现之前我们先来回顾一下 mutations 的使用. 将官方的 Vuex 导入进来,因为 ...

  8. Go 方法介绍,理解“方法”的本质

    Go 方法介绍,理解"方法"的本质 目录 Go 方法介绍,理解"方法"的本质 一.认识 Go 方法 1.1 基本介绍 1.2 声明 1.2.1 引入 1.2.2 ...

  9. .NET 8 IEndpointRouteBuilder详解

    Map ​ 经过对 WebApplication 的初步剖析,我们已经大致对Web应用的骨架有了一定了解,现在我们来看一下Hello World案例中仅剩的一条代码: app.MapGet(" ...

  10. 【Flutter】一文读懂混入类Mixin

    [Flutter]一文读懂混入类Mixin 基本介绍 Mixin是一种有利于代码复用,又避免了多继承的解决方案. Mixin 是面向对象程序设计语言中的类,提供了方法的实现,其他类可以访问 Mixin ...