Mybatis的原理你真的理解吗?源码解析持续更新

Mybatis源码初探

之前都只是在用Mybatis,但是一直没有深入的去了解过它的源码机制,趁着最近空闲时间比较多,再加上日常生活中用这框架比较多,就来简单探究下它的源码

Mybatis的简介

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。(来自中文官网介绍)

JDBC核心步骤

Mybatis、Hibernate、DATA JPA 都是基于JDBC驱动来做的,那么我们来看看JDBC的几个核心步骤是怎么做的?

  1. 加载驱动
  2. 获取连接
  3. 创建sql语句
  4. 获取statement对象
  5. 执行sql语句
  6. 处理结果集
  7. 关闭连接

Mybatis核心步骤

Mybatis的基本操作如何做的?

  1. 创建xml配置文件
  2. 创建mapper接口
  3. 需要SqlSessionFactory对象
  4. 获取SqlSession对象

此时我们通过最简单的方法来模拟Mybatis是如何解析sql最后查询出数据的

先给定一个mapper接口 (此处不使用xml形式和@Param注解的解析工作先做简单处理)

java 复制代码
public interface EmpMapper {

  @Select("select * from emp where empno = #{empno} and ename = #{ename}")
  List<Emp> selectEmpList( Integer empno,  String ename);

}

大家都知道,此时想调用selectEmpList方法,需要具体的实例对象,但是接口不能实例化**(所以此时我们需要动态代理,因为是接口,则使用JDK的动态代理)**

Proxy.newProxyInstance()方法创建了一个代理对象 empMapper,该代理对象实现了 EmpMapper 接口,并在 InvocationHandlerinvoke() 方法中提供了代理对象方法的实现逻辑。

java 复制代码
EmpMapper  empMapper = (EmpMapper) Proxy.newProxyInstance(MyMybatis.class.getClassLoader(), new Class[]{EmpMapper.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//        System.out.println(method.getName());
//        System.out.println(Arrays.asList(args));
        return null;
      }
    });
    empMapper.selectEmpList(7369,"SMITH");
  }

这里的method就是接口方法的名称,args就是传入的参数

但是此时想要查询语句,得要先获取到@Select注解上的sql语句,然后进行参数的替换

java 复制代码
EmpMapper  empMapper = (EmpMapper) Proxy.newProxyInstance(MyMybatis.class.getClassLoader(), new Class[]{EmpMapper.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 获取方法上的注解
        Select select = method.getAnnotation(Select.class);
        if (select!=null){
          //核心逻辑处理(获得sql语句)
          String[] value = select.value();
          String sql = value[0];
          System.out.println(sql);
        }
        return null;
      }
    });
    empMapper.selectEmpList(7369,"SMITH");
  }

此时的sql语句是

我们已经得到了sql语句,现在要获取参数的替换,但是由于args是个数组,我们也不知道如何跟下面的参数进行匹配,可以用map来处理

java 复制代码
EmpMapper  empMapper = (EmpMapper) Proxy.newProxyInstance(MyMybatis.class.getClassLoader(), new Class[]{EmpMapper.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      	// 通过参数来完成替换功能,因此需要先去解析参数
        Map<String, Object> stringObjectMap = parseArgs(method, args);
        System.out.println(stringObjectMap);
        // 获取方法上的注解
        Select select = method.getAnnotation(Select.class);
        if (select!=null){
          //核心逻辑处理(获得sql语句)
          String[] value = select.value();
          String sql = value[0];
          System.out.println(sql);
        }
        return null;
      }
    });
    empMapper.selectEmpList(7369,"SMITH");
  }


public static Map<String,Object> parseArgs(Method method,Object[] args){
    Map<String,Object> map = new HashMap<String,Object>();
    // 获取方法的参数数组
    Parameter[] parameters = method.getParameters();
    int index[] = {0};
    // 遍历获取所有参数名称
    Arrays.asList(parameters).forEach(parameter -> {
      String name = parameter.getName();
      // 参数名和参数值做一一匹配
      map.put(name,args[index[0]]);
      // 修改下标
      index[0]++;
    });
    return map;
  }

此时得到的结果,已经可以获取到参数对应的值

但是此时还要完成参数的替换,可以按照下面逻辑去处理(可以用其它思路来实现)

java 复制代码
public static String parseSql(String sql,Map<String,Object> map){
    // 作为字符追加
    StringBuilder builder = new StringBuilder();
    for(int i = 0;i<sql.length();i++){
      // 转为字符
      char c = sql.charAt(i);
      // 边界判断
      if (c == '#'){
        // 找到# 下一个位置 {
        int index = i+1;
        // 取出 {
        char nextChar = sql.charAt(index);
        // 边界判断
        if (nextChar!='{'){
          throw new RuntimeException("sql语句有错误,不是以{开头的");
        }
        StringBuilder argBuilder = new StringBuilder();
        // 此方法为解析{}里面的内容
        i = parseSqlArgs(argBuilder,sql,index);
        // 第一次循环完成 此时argName 应该是 empno
        String argName = argBuilder.toString();
        // 从map中取出key为empno的值
        Object value = map.get(argName);
        // 追加操作
        builder.append(value.toString());
        continue;
      }
      // 不符合#的字符 做追加操作
      builder.append(c);
    }
    return builder.toString();
  }

public static int parseSqlArgs(StringBuilder argBuilder,String sql,int index){
    index++;
    for (;index<sql.length();index++){
      // 取出索引位置的字符 此时第一次循环应该为{empno}中的 e
      char c = sql.charAt(index);
      // 边界判断
      if (c!='}'){
        // 字符追加
        argBuilder.append(c);
        // 继续
        continue;
      }
      if(c=='}'){
        return index;
      }
    }
    throw new RuntimeException("sql语句错误,没有以}结尾");
  }

此时已经可以看到打印出来的sql语句

紧接着就可以做以下操作了 (这也是Mybatis的核心处理操作)

  1. 数据库的操作(jdbc的解析)
  2. 结果集的映射处理
  3. ResultSet一个值一个值封装到对象中去
相关推荐
狂师17 分钟前
啥是AI Agent!2025年值得推荐入坑AI Agent的五大工具框架!(新手科普篇)
人工智能·后端·程序员
星辰大海的精灵19 分钟前
使用Docker和Kubernetes部署机器学习模型
人工智能·后端·架构
MikeWe22 分钟前
C++宏的解析:从基础语法到实战场景
后端
向往技术的猫菜27 分钟前
Java必需要会的MySQL知识
后端
Frank_zhou30 分钟前
Java代码是如何运行起来的
后端
Victor35630 分钟前
MySQL(121)如何处理死锁问题?
后端
亚洲第一中锋_哈达迪30 分钟前
深入剖析 go-zero 分布式缓存
后端
Frank_zhou32 分钟前
多线程-sleep-yield-join
后端
error_cn33 分钟前
sysctl监控系统性能
后端
小飞悟1 小时前
浏览器和服务器是怎么“认出你”的?揭秘 Cookie 认证
后端·node.js