Mybatis 通过接口实现 sql 执行原理解析

news2024/9/23 3:30:42

使用过 mybatis 框架的小伙伴们都知道,mybatis 是个半 orm 框架,通过写 mapper 接口就能自动实现数据库的增删改查,但是对其中的原理一知半解,接下来就让我们深入框架的底层一探究竟

1、环境搭建


首先引入 mybatis 的依赖,在 resources 目录下创建 mybatis 核心配置文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 环境、事务工厂、数据源 -->
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">
                <property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
                <property name="url" value="jdbc:derby:db-user;create=true"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 指定 mapper 接口-->
    <mappers>
        <mapper class="com.myboy.demo.mapper.user.UserMapper"/>
    </mappers>

</configuration>

在 com.myboy.demo.mapper.user 包下新建一个接口 UserMapper

public interface UserMapper {

    UserEntity getById(Long id);

    void insertOne(@Param("id") Long id, @Param("name") String name, @Param("json") List<String> json);
}

在 resources 的 com.myboy.demo.mapper.user 包下创建 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myboy.demo.mapper.user.UserMapper">

    <select id="getById" resultType="com.myboy.demo.db.entity.UserEntity">
        select * from demo_user where id = #{id}
    </select>

    <insert id="insertOne">
        insert into demo_user (id, name, json) values (#{id}, #{name}, #{json})
    </insert>
</mapper>

创建 main 方法测试

try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
    sqlSession = sqlSessionFactory.openSession();
    # 拿到代理类对象
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    # 执行方法
    UserEntity userEntity = mapper.getById(2L);
    System.out.println(userEntity);
    sqlSession.close();
}catch (Exception e){
    e.printStackTrace();
}

2、动态代理类的生成

🤔 通过上面的示例,我们需要思考两个问题:

  1. mybatis 如何生成 mapper 的动态代理类?

  1. 通过 sqlSession.getMapper 获取到的动态代理类是什么内容?

通过查看源码,sqlSession.getMapper() 底层调用的是 mapperRegistry 的 getMapper 方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // sqlSessionFactory build 的时候,就已经扫描了所有的 mapper 接口,并生成了一个 MapperProxyFactory 对象
    // 这里根据 mapper 接口类获取 MapperProxyFactory 对象,这个对象可以用于生成 mapper 的代理对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 创建代理对象
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

代码注释已经写的很清楚,每个 mapper 接口在解析时会对应生成一个 MapperProxyFactory,保存到 knownMappers 中,mapper 接口的实现类(也就是动态代理类)通过这个 MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession) 代码如下:

/**
 * 根据 sqlSession 创建 mapper 的动态代理对象
 * @param sqlSession sqlSession
 * @return 代理类
 */
public T newInstance(SqlSession sqlSession) {
    // 创建 MapperProxy 对象,这个对象实现 InvocationHandler 接口,里面封装类 mapper 动态代理方法的执行的核心逻辑
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

代码一目了然,通过 jdk 动态代理技术创建了 mapper 接口的代理对象,其 InvocationHandler 的实现是 MapperProxy,那么 mapper 接口中方法的执行,最终都会被 MapperProxy 增强

3、MapperProxy 增强 mapper 接口

MapperProxy 类实现了 InvocationHandler 接口,那么其核心方法必然是在其 invoke 方法内部

/**
 * 所有 mapper 代理对象的方法的核心逻辑
 */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 如果执行的方法是 Object 类的方法,则直接反射执行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // 1、根据method创建方法执行器对象 MapperMethodInvoker,用于适配不同的方法执行过程
        // 2、执行方法逻辑
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
}

3.1、cachedInvoker(method)

由于 jdk8 对接口增加了 default 关键字,使接口中的方法也可以有方法体,但是默认方法和普通方法的反射执行方式不同,需要用适配器适配一下才能统一执行,具体代码如下

/**
 * 适配器模式,由于默认方法和普通方法反射执行的方式不同,所以用 MapperMethodInvoker 接口适配下
 * DefaultMethodInvoker 用于执行默认方法
 * PlainMethodInvoker 用于执行普通方法
 */
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        // 返回默认方法执行器 DefaultMethodInvoker
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        }
        // 返回普通方法执行器,只有一个 invoke 执行方法,实际上就是调用 MapperMethod 的执行方法
        else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
}

如果判定执行的是接口的默认方法,则原始方法封装成 DefaultMethodInvoker,这个类的 invoke 方法就是利用反射调用原始方法,没什么好说的

如果是普通的接口方法,则将方法封装成封装成 MapperMethod,然后再将 MapperMethod 封装到 PlainMethodInvoker 中,PlainMethodInvoker 没什么好看的,底层的执行方法还是调用 MapperMethod 的执行方法,至于 MapperMethod,咱们放到下一章来看

3.2、MapperMethod

首先看下构造方法

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 通过这个 SqlCommand 可以拿到 sql 类型和sql 对应的 MappedStatement
    this.command = new SqlCommand(config, mapperInterface, method);
    // 包装了 mapper 接口的一个方法,可以拿到方法的信息,比如方法返回值类型、返回是否集合、返回是否为空
    this.method = new MethodSignature(config, mapperInterface, method);
}

代码里的注释写的很清楚了,MapperMethod 构造方法创建了两个对象 SqlCommand 和 MethodSignature

mapper 接口的执行核心逻辑在其 execute() 方法中:

/**
   * 执行 mapper 方法的核心逻辑
   * @param sqlSession sqlSession
   * @param args 方法入参数组
   * @return 接口方法返回值
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        // 参数处理,单个参数直接返回,多个参数封装成 map
        Object param = method.convertArgsToSqlCommandParam(args);
        // 调用 sqlSession 的插入方法
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 方法返回值为 void,但是参数里有 ResultHandler
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          // 方法返回集合
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          // 方法返回 map
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          // 方法返回指针
          result = executeForCursor(sqlSession, args);
        } else {
          // 方法返回单个对象
          // 将参数进行转换,如果是一个参数,则原样返回,如果多个参数,则返回map,key是参数name(@Param注解指定 或 arg0、arg1 或 param1、param2 ),value 是参数值
          Object param = method.convertArgsToSqlCommandParam(args);
          // selectOne 从数据库获取数据,封装成返回值类型,取出第一个
          result = sqlSession.selectOne(command.getName(), param);

          // 如果返回值为空,并且返回值类型是 Optional,则将返回值用 Optional.ofNullable 包装
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

代码逻辑很清晰,拿 Insert 方法来看,他只做了两件事

  1. 参数转换

  1. 调用 sqlSession 对应的 insert 方法

3.2.1、参数转换 method.convertArgsToSqlCommandParam(args)

在 mapper 接口中,假设我们定义了一个 user 的查询方法

List<User> find(@Param("name")String name, @Param("age")Integer age)

在我们的 mapper.xml 中,写出来的 sql 可以是这样的:

select * from user where name = #{name} and age > #{age}

当然不使用 @Param 注解也可以的,按参数顺序来

select * from user where name = #{arg0} and age > #{arg1}
或
select * from user where name = #{param1} and age > #{param2}

因此如果要通过占位符匹配到具体参数,就要将接口参数封装成 map 了,如下所示

{arg1=12, arg0="abc", param1="abc", param2=12}
或
{name="abc", age=12, param1="abc", param2=12}

这里的这个 method.convertArgsToSqlCommandParam(args) 就是这个作用,当然只有一个参数的话就不用转成 map 了, 直接就能匹配

3.2.2、调用 sqlSession 的方法获取结果

真正要操作数据库还是要借助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param) 方法的执行,其第一个参数是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 标签的 id的组合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById,第二个参数就是上面转换过的参数,至于 sqlSession 内部处理逻辑,不在本章叙述范畴

sqlSession 方法执行完后的执行结果交给 rowCountResult 方法处理,这个方法很简单,就是将数据库返回的数据处理成接口返回类型,代码很简单,如下

  private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.returnsVoid()) {
      result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
      result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
      result = (long) rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
      result = rowCount > 0;
    } else {
      throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
  }

4、小结


到目前为止,我们已经搞清楚了通过 mapper 接口生成动态代理对象,以及代理对象调用 sqlSession 操作数据库的逻辑,我总结出执行逻辑图如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/181848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Consul服务注册与发现

目录 一、Consul简介 &#xff08;一&#xff09;官网 &#xff08;二&#xff09;特点 二、安装并运行Consul &#xff08;一&#xff09;官网安装说明 &#xff08;二&#xff09;下载 &#xff08;三&#xff09;使用开发模式启动 三、服务提供者 四、服务消费者 …

怎么压缩pdf文件?选对方法其实很简单!

相信许多人在使用设备的时候都会面对这样一个问题&#xff0c;那就是设备内存不足。仿佛不管我们多么努力的节省空间&#xff0c;总是会到头来遇到储存空间不足得难题&#xff0c;尤其是一些比较大的pdf文件&#xff0c;特别占据我们的设备内存&#xff0c;那么你知道怎么压缩p…

0128 Web API基本认知

作用使用JS去操作html和浏览器分类DOM&#xff08;文档对象模型&#xff09;BOM&#xff08;浏览器对象模型&#xff09;DOM是什么&#xff1a;Document Object Model-----文档对象模型&#xff0c;用来呈现以及与任意HTML或XML文档交互的API&#xff0c;浏览器提供的一套专门用…

【Linux】一文掌握Linux权限

环境&#xff1a;centos7&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅&#x1f339;前言权限 一词相信大家都不陌生&#xff0c;与我们的生活密切相关。小区里的门禁制度、公司里的管理制度、学校里的校规规定、甚至是社交平台上的一些设置…

python 手机相机传感器信息计算

传感器信息计算 输入传感器尺寸以上已红米12pro为例 输入传感器尺寸 1/1.4英寸 0.7142857 输入像素2亿 200000000 得到以下结果 和宣传中的传感器信息一致 附源码 import sympyclass CMosInfo(object):"""传感器信息计算"""def __init__(…

C 程序设计教程(19)—— 数组和指针(二):字符数组与字符串

C 程序设计教程&#xff08;19&#xff09;—— 数组和指针&#xff08;二&#xff09;&#xff1a;字符数组与字符串 该专栏主要介绍 C 语言的基本语法&#xff0c;作为《程序设计语言》课程的课件与参考资料&#xff0c;用于《程序设计语言》课程的教学&#xff0c;供入门级用…

word实用操作:几个关于录入和排版的小妙招

对于职场人来说&#xff0c;工作中使用Word早已成了习惯。而如何提高Word的技术&#xff0c;那简直是职场人一生都需要研究探索的课题。因此&#xff0c;今天小编将为大家分享几个实用的Word小技巧&#xff0c;这些技巧貌似不起眼&#xff0c;但是学会后&#xff0c;可以大大提…

如何查linux服务器的带宽占用?哪些进程占用带宽?

前言操作系统&#xff1a; Linux操作环境&#xff1a; Centos7 / ubuntulinux查看服务器带宽具体方法   一、使用speedtest-cli命令查看下载和上传最大流量值因为命令是python的&#xff0c;所以需要先下载一个python&#xff0c;用pip下载次命令&#xff1b;123yum -y insta…

七个 Vue 项目用得上的 JavaScript 库分享

文章目录前言一、vueuse二、vue-js-modal三、vue-wait四、good-table五、vue-notification六、tree select七、egjs-infinite grid总结前言 借助开源库加速 Vue 项目的开发进度是现代前端开发比较常见的方式&#xff0c;平常收集一些 JavaScript 库介绍&#xff0c;在遇到需要的…

上古神兵,先天至宝,Win11平台安装和配置NeoVim0.8.2编辑器搭建Python3开发环境(2023最新攻略)

毫无疑问&#xff0c;我们生活在编辑器的最好年代&#xff0c;Vim是仅在Vi之下的神级编辑器&#xff0c;而脱胎于Vim的NeoVim则是这个时代最好的编辑器&#xff0c;没有之一。异步支持、更好的内存管理、更快的渲染速度、更多的编辑命令&#xff0c;是大神Thiago de Arruda对开…

第九层(5):STL之stack

文章目录前情回顾stack概念stack容器需要注意的地方stack类内的构造函数stack类内的赋值操作stack类内的插入stack类内的删除stack类内的访问stack类内的大小操作下一座石碑&#x1f389;welcome&#x1f389; ✒️博主介绍&#xff1a;一名大一的智能制造专业学生&#xff0c;…

Knowledge-based-BERT(三)

多种预训练任务解决NLP处理SMILES的多种弊端&#xff0c;代码&#xff1a;Knowledge-based-BERT&#xff0c;原文&#xff1a;Knowledge-based BERT: a method to extract molecular features like computational chemists&#xff0c;代码解析继续downstream_task。模型框架如…

HTML中的div和span标签

<!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>HTML中的div和span标签</title> </head> <body> <!-- 1、div和span是什么&#xff1f;有什么用…

【linux】文件操作(IO)详述

文件操作一、引入二、系统调用接口2.1 open与close2.2 write2.3 read三、文件描述符3.1 分配规则四、重定向4.1 输出重定向4.2 追加重定向4.3 输入重定向4.4 独立性五、缓冲区5.1 缓冲区刷新策略5.2 缓冲区位置5.3 现象解释六、文件系统6.1 文件系统分区6.1.1 分区图6.1.2 介绍…

实时推荐业务介绍 grpc接口对接

5.1 实时推荐业务介绍 学习目标 目标 无应用 无 5.1.1 实时推荐逻辑 逻辑流程 1、后端发送推荐请求&#xff0c;实时推荐系统拿到请求参数 grpc对接2、根据用户进行ABTest分流 ABTest实验中心&#xff0c;用于进行分流任务&#xff0c;方便测试调整不同的模型上线3、推荐中心…

k8s之部署有状态应用

写在前面 本文一起看下k8s对于有状态应用部署提供的解决方案。 1&#xff1a;有状态应用和无状态应用 如果是一个应用每次重启时依赖环境都能和第一次启动时的完全一致&#xff0c;则就可以称这类应用是无状态应用用&#xff0c;反之&#xff0c;就是有状态应用&#xff0c;如…

自动写代码的AI工具,已经支持 VsCode 插件安装使用

自动写代码的AI工具&#xff0c;已经支持 VsCode 插件安装使用&#xff0c;它的功能并不是「代码补全」&#xff0c;而是「代码生成」。 之前有个比较火的 GitHub Copilot&#xff0c;但是这是商业产品&#xff0c;并且没有开源&#xff0c;现在又被告了。 GitHub Copilot 面…

SQLSERVER 事务日志的 LSN 到底是什么?

一&#xff1a;背景 1. 讲故事 大家都知道数据库应用程序 它天生需要围绕着数据文件打转&#xff0c;诸如包含数据的 .mdf&#xff0c;事务日志的 .ldf&#xff0c;很多时候深入了解这两类文件的合成原理&#xff0c;差不多对数据库就能理解一半了&#xff0c;关于 .mdf 的合…

代码随想录--二叉树章节总结 Part II

代码随想录–二叉树章节总结 Part II 1.Leetcode222 求完全二叉树结点的个数 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达…

Python机器学习:特征变换

&#x1f315; 特征变换 特征变换主要就是针对一个特征&#xff0c;使用合适的方法&#xff0c;对数据的分布、尺度等进行变换&#xff0c;以满足建模时对数据的需求。 特征变换可分为数据的数据的无量纲化处理和数据特征变换。 &#x1f317; 数据的无量纲化处理 常用处理…