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一个值一个值封装到对象中去
相关推荐
子兮曰11 小时前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰11 小时前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝12 小时前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码12 小时前
别再前后端各写一套表单校验了
java·后端
大勇前进13 小时前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu13 小时前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile13 小时前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白8013 小时前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙13 小时前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发14 小时前
软件工程SOLID 五大设计原则
后端·软件工程