首页 > 编程语言 >Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler

Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler

时间:2022-08-21 12:11:28浏览次数:46  
标签:插件 ResultSetHandler ResultMap class 源码 mybatis 拦截 public

Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler

一丶前情回顾

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

image-20220501114216332

  • prepareStatement

    image-20220501114455271

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

      image-20220501114518405

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

    2. 根据连接准备Statement

      image-20220501114859479

      • instantiateStatement

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

      • setStatementTimeout

      image-20220501115120437

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

    3. 给占位符设置参数

      这部分使用DefaultParameterHandler的setParameters 方法

      image-20220501115452946

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

      image-20220501115812246

    至此我们以及将参数设置到PreparedStatement 上了
    相当于
    select * from A where a=#{a}
    这里的a=?已经成功设置上去了
    

二丶执行查询

这些查询交由StatementHandler处理,可以看下图,

  • 对于PreparedStatement就是调用其execute方法

    image-20220501120132434

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

    image-20220501120321218

  • 其他的Statement

    image-20220501120432561

    获取sql 执行

还有个RoutingStatementHandler,就是调用被装饰着的query方法

三丶处理结果集

终于到了这篇总结的重点,处理结果集,上面图可以看到Statement执行excute后,使用ResultSetHandler处理结果集,ResultSetHandler只有一个实现类 DefaultResultSetHandler

1.处理多结果集

存储过程存在多结果集的情况,

image-20220501121329328

2.处理结果集

image-20220501121738175

image-20220501121927782

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

image-20220501122810392

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

image-20220501123831384

这里的自动映射应该是处理,没有指定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对象是

image-20220501124646074

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

image-20220501124146089

四丶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动态代理

image-20220501125328505

image-20220501125402309

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

image-20220501125734412

注意获取方法的方式

Method method = sig.type().getMethod(sig.method(), sig.args());

getMethod方法是没有办法获取到私有方法的,所有无法拦截一个私有方法

  • 获取被代理类的接口

image-20220501133755874

  • 动态代理对象生成

image-20220501133853692

image-20220501133944011

3.动态代理生成的对象是怎么被使用的

如上我们知道了mybatis 是怎么支持插件的,根据拦截器上的信息生成动态代理对象,动态代理对象在执行方法的时候会进入拦截器的intercept拦截方法,那么动态代理的生成的对象在哪里被使用到昵

  • Configuration 类 也就是mybatis的大管家,在new一些mybatis四大对象的时候会使用到插件

image-20220501134455505

image-20220501134505350

image-20220501134517349

image-20220501134540586

也就是说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;
        }
    }
    
  • 存在表结构

    image-20220501135017820

  • 我希望在插入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方法

    image-20220501140914927

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

  3. 实现自定义ResultSetHandler

    上面我们企图拦截,发现可行但是不太好,我们看看 ResultSetHandler使用的地方,在StatementHandler实现类的 query方法,最后调用ResultHandler处理结果集

    image-20220501141255056

    我们能不能自己实现一个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方法会使用到这些成员变量,所有我们需要把这些属性设置上去,属性来自于哪儿昵

    image-20220501141808064

    如上是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方法

image-20220501142325072

@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>

最后成功反序列化且类型没有错误

标签:插件,ResultSetHandler,ResultMap,class,源码,mybatis,拦截,public
From: https://www.cnblogs.com/cuzzz/p/16609763.html

相关文章

  • Mybatis源码1JDBC->mybatis主要流程->mybatis Excutor简介
    Mybatis源码1JDBC->mybatis主要流程->mybatisExcutor简介一丶mybatis概述MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎......
  • Spring源码学习笔记4——BeanFactoryPostProcessor执行
    一丶BeanFactoryPostProcessor是什么Spring留给我们的一个扩展接口,在BeanDefinition加载注册完之后,并执行一些前置操作(笔记3)之后会反射生产所有的BeanFactoryPostProcesso......
  • Spring源码学习笔记6——Spring bean的实例化
    一丶前言前面我们了解到读取xmlor根据扫描路径生成BeanDefinition并注册到BeanFactory,相当于我们具备了生火做饭的原材料:BeanDefinition,接下来就是Spring最为核心的,根据......
  • Tomcat源码分析--类加载器
    Tomcat类加载器结构上图是Tomcat文档中所展示的Tomcat类加载结构。在这个结构中Bootstartap和System的类加载器由java虚拟机实现。common类加载器由Tomcat容器实现,它对......
  • Notepad plus 通过NppExec插件编译/运行 golang,php,python等语言
        1. 在Notepadplus的插件-->插件管理中,添加nppExec插件。          2.打开插件-->NppExec,选择Showconsole,和Follow($CURRE......
  • Mybatis的一级、二级缓存
    一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Sessionflush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。二级......
  • 好看实用的日历插件
    我在工作中,前端老大哥给了我个需求,让我完成个样式精美的日历,并且确定每天打卡情况,然后完成之后说是日历太丑了,我然后我去网上找了找好看的,,最后在b站上看到一个,样式确实不错......
  • 运动及运动封装、swiper插件
    运动概述运动主要是动画的操作,主要是操作某个document元素的属性变化(位置变化)运动主要的三步骤使用定时器来定时更改对应的内容实时获取对应的元素的属性及相关内容......
  • 大家都能看得懂的源码 - ahooks useSet 和 useMap
    本文是深入浅出ahooks源码系列文章的第十篇,该系列已整理成文档-地址。觉得还不错,给个 star 支持一下哈,Thanks。今天我们来聊聊ahooks中对Map和Set类型进行状态......
  • Nik Collection 5 for Mac(PS滤镜插件套装)中文版
    NikCollection5formac是一款功能强大的图片处理插件集,其包含了七款ps插件,功能涵盖修图、调色、降噪、胶片滤镜等方面。NikCollection作为很多摄影师和摄影爱好者所熟......