【源码剖析】Mybatis 详解

shadowLogo

在之前的博文中,本人分别讲解了 SpringSpring MVC核心源码
那么,在本篇博文中,本人就来讲解下 Mybatis核心源码

本人先来给出一个 API使用案例

API调用:

首先是 实体类(O)

实体类(O):

package edu.youzg.pojo;

import java.io.Serializable;

public class Account implements Serializable {
    private Integer id;
    private String name;
    private Integer money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }

}

接着是 mapper(R)

mapper(R):

AccountMapper 类:

package edu.youzg.mapper;

import edu.youzg.pojo.Account;

public interface AccountMapper {
    Account selectById(Integer id);
}

AccountMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="edu.youzg.mapper.AccountMapper">

	<!--开启 二级缓存-->
    <cache/>

    <resultMap id="result" type="edu.youzg.pojo.Account">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="money" jdbcType="INTEGER" property="money"/>
    </resultMap>

    <select id="selectById" resultMap="result">
        select id,name,money from account
        <where>
            <if test="_parameter > 0">
                and id=#{_parameter}
            </if>
        </where>
    </select>
</mapper>

接着是 数据库信息(M)

数据库信息(M):

数据库


然后是 配置类

配置类:

db.properties:

mysql.driverClass=com.mysql.cj.jdbc.Driver
mysql.jdbcUrl=jdbc:mysql://localhost:3306/explore_source?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
mysql.user= root
mysql.password= 123456

mybatis-config.xml:

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

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
           <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${mysql.driverClass}"/>
                <property name="url" value="${mysql.jdbcUrl}"/>
                <property name="username" value="${mysql.user}"/>
                <property name="password" value="${mysql.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="edu/youzg/mapper"/>
    </mappers>
</configuration>

最后,是 测试类

测试类:

package edu.youzg.demo;

import edu.youzg.mapper.AccountMapper;
import edu.youzg.pojo.Account;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;

/**
 * @Author: Youzg
 * @CreateTime: 2021-03-28 14:29
 * @Description: 带你深究Java的本质!
 */
public class YouzgDemo {

    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        Reader reader;
        try {
            //将XML配置文件构建为Configuration配置类
            reader = Resources.getResourceAsReader(resource);
            // 通过加载配置文件流构建一个SqlSessionFactory  DefaultSqlSessionFactory
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            // 数据源 执行器  DefaultSqlSession
            SqlSession session = sqlMapper.openSession();
            try {
            	// 获取mapper代理
                AccountMapper mapper = session.getMapper(AccountMapper.class);
                // 执行查询 底层执行jdbc
                Account account = mapper.selectById(2);
                session.commit();
                System.out.println(account);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                session.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

那么,本人来展示下 运行结果
运行结果


接下来,本人就来讲解下 Mybatis核心源码

首先,本人来通过 一张图,来展示下 Mybatis功能架构

功能架构:

总体架构


从上图中,我们也能看出:

我们所有的 SQL执行,都是 基于Statement

因此,Mybatis的核心源码,可分为 解析配置信息执行SQL 两大类

那么,本人先来讲讲 解析配置信息 这部分代码:

解析配置信息:

我们从上面的 测试类 中可以看得出:

读取 配置类,并进行 解析 是在

//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory  DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

的 代码块 中进行的

其实,主要的 解析操作,还是靠

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

这行代码执行的

那么,我们来看看 build()方法 做了什么:

解析 配置内容 —— build()方法:

包装
我们可以看到:

这是一个 包装方法,并没有什么 执行逻辑

我们跟进去,来看看具体是怎么实现的:
再次包装
我们继续跟进去:
创建工厂类
我们可以看到:

build()方法 本质上 创建了一个 DefaultSqlSessionFactory类 对象,并且将配置类中的所有内容,注册到了 DefaultSqlSessionFactory类 对象 中


那么,build()方法 的参数 Configuration类 的构成是什么呢?

配置内容 的 封装 —— Configuration类:

1
2
3
可以看到:

基本上 所有配置文件的信息 都可以封装在 Configuration类


而 这个 Configuration类 对象,是 通过调用 XMLConfigBuilder类parse()方法 解析得出

那么,本人来展示下 parse()方法 的内容:
parse方法

封装 配置信息 —— parseConfiguration()方法:

解析配置


那么,具体是怎么解析的呢?
我们来看下几个比较常用的配置的解析:

propertiesElement标签 的 解析:

解析 properties标签


typeAliase标签 的 解析:

别名解析
我们一直跟下去,就会看到如下代码:
map存储


environments标签 的 解析:

解析数据源


mappers标签 的 解析:

mappers标签解析
那么,我们再来看看

mapperParser.parse();

这行代码,底层是如何实现的:
具体执行


那么,至此,配置信息就解析完毕了!

本人再来总结下 解析配置信息 的流程:

解析配置 流程图:

解析配置


在我们 解析完配置 后,就到了 获取SqlSession 的环节了:

获取 SqlSession:

包装
我们可以看到:这是一个 包装方法,我们继续跟进去:
真实


我们来看看 上图中 96行 代码,发生了什么:
96
可以看到:

执行器 根据 设置的类型 构建

在这里,本人要提醒的一点是:

本方法中,使用了 装饰器设计模式

那么,这 四种执行器,有什么 区别 呢?
本人将在 文末 进行讲解!


那么,本人在这里,通过一张图总结下 获取sqlsession流程图

获取sqlsession 流程图:

流程


在上面的代码中,解析了配置创建了sqlsession

接下来,本人就讲解下 获取mapper 的过程:

获取 mapper代理:

我们来跟进示例代码的 32行
32
我们来跟进看看是不是 在Map中查询的:
继续
我们继续跟进:
底层原理
我们可以看到:

在底层,是通过 接口类型,获取 对应的mapper代理工厂
之后,通过 对应的mapper代理工厂参数所传sqlsession 创建了 mapper代理


那么,本人在这里,通过一张图总结下 获取mapper流程图

获取mapper代理 流程图:

流程


最后,本人就讲解下 调用执行sql 的过程:

调用执行sql:

本人在 示例代码35行 打上断点,来看看具体是怎么执行的:
执行

跟着断点执行,就会发现:

上面方法真正执行的是 最下面两行代码

获取缓存映射对象 —— cachedMapperMethod()方法:

查询缓存


那么,从缓存中获取之后,我们再来看看之后会如何执行:

sql执行原生API 的调用 —— execute()方法:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {	// 根据sql类型,分别进行不同的执行流程
    case INSERT: {
    Object param = method.convertArgsToSqlCommandParam(args);
      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()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);	// 转换参数
        result = sqlSession.selectOne(command.getName(), param);	// 调用sqlsession的api
      }
      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;
}

可以看到:

执行流程,先 转换了参数,然后 调用sqlsession的sql执行API


我们继续往下跟:
执行器执行

在上一步中,调用了 executorquery()方法
但是,此处的 executor 并不是 JDBC提供的,而是 封装继承JDBC所提供的executor

那么,缓存的功能,在底层是如何实现的呢?

缓存的实现(仅 配置开启缓存) —— executor的使用:

我们来跟着断点,来看看 是如何实现缓存的:
缓存调用
在上图中,我们可以看出:

若我们 开启了缓存
查询 时,就会先从缓存中获取
没有取到,则会交由 真正的执行sql执行器 去执行,并将 执行结果 存入 二级缓存

我们来跟进上图中 101行 代码:
包装
我们可以看到:这是一个 包装方法

继续跟进:
缓存实现
我们可以看到:

首先,查询了 二级缓存
然后,若没查到,就设置一个值 防止 缓存穿透
再 根据 标识,决定是否返回 缓存中查询到的信息

顺带一提:
当我们执行 增删改 操作时,底层会调用如下方法:
RUD
我们能看到:

当执行 增、删、改 操作 时,就会 清空缓存

到此,相信就能完全验证本人之前博文《【Mybatis框架 学习】缓存 详解》的内容!


那么,本人在这里,通过一张图总结下 调用执行sql流程图

调用执行sql 流程图:

总结


在上文中,本人也有一些问题留在文末进行讲解
那么,本人来总结讲解下 这些问题:

问题讲解:

四种执行器 有什么区别?

答曰:

  • SimpleExecutor
    每执行一次 updateselect,就 开启一个Statement对象用完立刻关闭 Statement对象
  • ReuseExecutor
    执行 updateselect,以 sql作为key 查找Statement对象,存在就使用,不存在就创建
    用完后不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用
    简言之,就是 重复使用Statement对象
  • BatchExecutor
    执行 update(没有select,JDBC批处理不支持select),将 所有sql 都添加到 批处理 中(addBatch()),等待 统一执行(executeBatch()),
    缓存多个Statement对象每个Statement对象 都是 addBatch()完毕 后,等待 逐一执行executeBatch()批处理
    JDBC批处理 相同
  • CacheExecutor
    其实是封装了普通的Executor
    和 普通的 区别 是:

在查询前先会查询缓存中是否存在结果,

  • 如果 存在,就 使用 缓存 中的结果,
  • 如果 不存在,还是 使用 普通的Executor 进行查询,再将查询出来的 结果 存入 缓存

在我们学习 Mybatis 时,了解过 防止缓存溢出 的 几大移除策略
那么,本人在这里讲解下 这些策略 的 底层实现

缓存移除 策略:

SynchronizedCache:

同步
我们可以看到:

凡是 操作缓存 的方法,都加上了 synchronized关键字,保证了 线程安全


统计命中率以及打印日志 —— LoggingCache:

log


过期清理缓存区 —— ScheduledCache:

定时清理


最近最少使用 —— LruCache:

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);	// 向 缓存 中 添加 新键值对
    cycleKeyList(key);	// 判断是否 要删除最久键值对
  }

  // 每次访问,都会遍历一次key进行重新排序,将访问元素放到链表尾部。
  @Override
  public Object getObject(Object key) {
    keyMap.get(key); // 更新最后访问时间
    return delegate.getObject(key);	// 从缓存中,获取key代表的键值对的值
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);	// 删除 指定key的键值对
  }

  @Override
  public void clear() {
    delegate.clear();	// 清空 责任链列表
    keyMap.clear();	// 清空 keyMap
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);	// 删除 最久未访问的key 的键值对
      eldestKey = null;	// 将 eldestKey 置为 null
    }
  }

}

说到 LRU,那可是 面试常问点
Mybatis 中,实现也很简单 —— 利用 LinkedHashMap 的性质:

  • 添加新元素 时,调用removeEldestEntry()方法,若返回true,则 删除最久未使用的元素
  • 访问一个元素 时,会将 当前键值对 放于 链表末尾(更新最后访问时间)

希望同学们在看完本人的解析后,能有所感悟!


先进先出 —— FifoCache:

public class FifoCache implements Cache {

  private final Cache delegate;
  private final Deque<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(int size) {
    this.size = size;
  }

  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);	// 先 判断是否超出范围
    delegate.putObject(key, value);	// 再 向缓存中添加键值对
  }

  @Override
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyList.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  private void cycleKeyList(Object key) {
    keyList.addLast(key);	// 将 新元素的键,放入keyList的末尾
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();	// 获取 最早入队的键
      delegate.removeObject(oldestKey);	// 删除 最早的键值对
    }
  }

}

FIFO策略 的 实现步骤,和 LRU策略 的实现步骤十分类似:

录入前,先判断是否 超出范围,若 超出范围,则 先删除最早的键值对再添加新键值对


那么,至此,Mybatis 的核心源码,就解读完毕了!

写了这么多篇 SSM 框架 的源码解读博文,最后才发现 Mybatis的源码反而是最简单易懂的
那么,秋招前可能不会再更新博文了,也可能在学习一些很重要的知识点时想要深挖会写几篇

在得闲时,本人会继续写些更有价值的博文,觉得有帮助的提醒不要吝啬关注和赞哦!
累了

posted @ 2021-03-30 00:40  在下右转,有何贵干  阅读(296)  评论(0编辑  收藏  举报