我终于到清楚了Mybatis的运行机制

Mybatis源码

Mybatis是我们常用的一个框架,主要用于数据库的操作,在以往的文章中已经写过Mybatis的源码分析、以及操作。在今天的这篇文章中,主要用自己的语言描述一下自己对于Mybatis源码的认识与理解,不去做具体的源码分析,具体的源码分析可以参考:

Mybatis原理六大步骤详细解析_码涤生的博客-CSDN博客

一、Mybatis源码概述

在说Mybatis源码之前我们先来阐述一下Mybatis使用的几种过程与方法,Mybatis的使用我们可分为注解类型以及xml形式,在使用Mybatis的时候我们通常都是融合了其他的框架——Spring、SpringBoot等,这些框架也已经帮我们做了很多操作。那么如果要是仅仅使用Mybatis的话我们要做什么操作——利用~Builder来构造一个SqlSessionFactory工厂,再利用这个工厂来生成Mapper的代理。最终我们在Service层使用的时候是通过这个Mapper代理调用具体的方法。在接下来的源码分析中我们就从一个简单的select语句说起,来描述一下他的执行流程。

Mybatis的执行流程可以概述为:获取代理对象、构建方法对象、SQL指令跳转、缓存处理、SQL语句执行、结果集处理。这六个步骤环环相扣。我们知道使用了Mybatis的dao层的接口是没有具体的实现的他通过注解或是xml的形式绑定了SQL语句。那么如果想要这些SQL语句获得执行系统应该如何做呢?那就是借用Mybatis提供的能力,这里使用到的技术就是Java代理,当我们通过**sqlSession.getMapper(XXXMapper.class)**获取了Mapper接口的代理对象,我们可以理解这时候我们有了进入Mybatis的钥匙,我们看是执行具体的操作——方法,这个方法是一个单独的个体,在Mybatis的世界作为一个个体我想要做一些操作那么我也需要一些自己的信息——我来自哪里、我要做什么、我手上有什么、我想要什么、我的身份ID是什么,第二部的MapperMethod就是做这样的一个操作。有了这些操作我们就开始自己的行为之路,走在Mybati的十字路口我改何去何从,这时候会在第三步中根据个体自己的一些信息来决定我要走那条路,也就是SQL指令跳转,这里面他也用了一个时髦而又真实的概念路由。走过迷茫的十字路口来到了属于自己的数据殿堂,在这里他的一个逻辑就是如果我想要的东西已经有了我就直接拿了回去,如果要是没有的话我就根据自己的规则造一些。那么到底我要不要将拿到的数据做一下缓存,供他人用或是自己用这里面就牵扯到缓存策略,在Mybatis中也存在两级缓存,连接缓存的概念就不用说了,他的理念类似于并发概念中的多级缓存(不恰当的使用会造成共享变量的内存可见性问题)。如果没有在缓存中获取到自己想要的数据那么就会进入第五步的SQL语句执行,Mybatis中SQL语句的执行最终还是使用了JDBC的相关操作,在这里面Mybatis会调用参数处理模块解析并入参处理。不管是缓存还是说直接通过sql语句产寻获得的数据最终都会根据我们的配置来对结果集,进行分装或是处理。在MybatisPlus中我们可以在相关的操作中传入一个Function,会对查询出来的结果做一个处理——或是映射。那么这个Function个人理解应该是作用在Mybatis结果集处理模块之后。

二、六大步骤执行描述

1、获取代理对象

我们通过SqlSession来获取指定dao层接口的Mapper对象,SQLSession默认的是实现类就是DefaultSqlSession,通过调用他的getMapper方法传递接口的clsas对象进去,他又会经过Configuration以及MapperRegistry对象来生成Mapper。在Configuration中提供了一同中的一些配置,而在MapperRegistry中通过配置的文件来检查是否有对应Mapper的xml文件,这个文件中保存的是与数据库操作有关到 sql语句信息,如果没有的话会进行报错。如何去检查实际上是通过一个key为class的map,**但是这个map中的数据是什么时候生成填充进去的,这个一直不知很理解。**如果要是没有在map中找到对应的SQL配置信息(有的回收可能是通过注解实现的),则会报错,查找成功返回的类型是MapperProxyFactory,通过这个工厂对象就可以生成最终想要的代理。

return mapperProxyFactory.newInstance(sqlSession);

❓ 这里面处理的思路不能理解重点的是后边要结合代理的知识理解一下Mybatis是如何根据相应的信息来生成代理的

在Mybatis中我们使用的是单接口动态代理,代理生成是要通过Proxy的newProxyInstance,他的三个入参——**目标类的来加载器、目标类的接口集、目标类的处理器对象。**在Mybatis中目标类的类加载器是初始化MapperProxyFactory的时候就已经生成了的:

// 用于创建实现该mapperInterface接口的代理对象
private final Class<T> mapperInterface;

利用这个mapperInterface就可以获得第一第二个参数,第三个参数是目标对象的处理器也就是——MapperProxy,第三个参数需要我们构造,主要是通过有参构造函数关联了sqlSession、mapperInterface以及存储method与ProxyMethod的methodCache。

Mybatis这一块关代理的生成与处理是很值得学习的,通过这里可以学会在应用开发中代理的使用技巧,以及真个代码格式的设计,其中还讲到了关于多接口代理的实现——自己原来实现的太low了,有关Java代理的可以参考下文:

Java代理——静态代理、单接口动态代理、多接口动态代理、AOP编程_码涤生的博客-CSDN博客_动态代理多个接口

2、构建方法对象

上面返回了一个MapperProxy的对象进过类型装换转换成了我们自定一个Mapper类型,这时候我们相当于是有了进入Mybatis的一把钥匙。可以持有这个语句柄来调用我们自定义的在Mapper中定义的查询方法。由于使用了动态代理所以这时候对我们方法的调用就进入到了Mybatis为我们封装的MapperProxy的invoke方法中。在Java中所有的对象都继承自Object从哪里也继承了很多的方法,在Java中还有很多的native方法,在invoke中首先会检查调用的是否为这些方法如果是的话就不需要再执行后面有关数据库相关的操作了,Mybatis会直接让他执行他本身的方法如果要是正常定义的方法则会封装以及生成一个围绕这个方法相关的详细类——MapperMethod(有与这个数据库动作相关的所有信息)。生成MapperMethod是也是有一层缓存的概念,如果methodCache中存在的话会直接返回,没有的话则进行创建并且将生产成的MapperMethod放入methodCache中。

// eg1: mapperInterface = interface mapper.UserMapper
//      method = public abstract vo.User mapper.UserMapper.getUserById(java.lang.Long)
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
}

在SqlCommand中有两个值一个是MappedStatement的唯一标识name一个是SqlCommond类型的type(UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;)其中的MappedStatement是用于保存映射器的一个节点时间上也就是我们的一个方法,在这里面包含了我们配置的诸多信息——SQL的id、缓存信息、resultMap、parameterType、resultType、languageDriver等重要配置内容。

在解析MappedStatement的时候数据来源是Configuration,存储MapperStatement的是一个以方法全签名为key的map。由于调用的方法可能继承与父类所以说在第一次用本类的签名找不到的时候就会一层层的朝上遍历他的父类(接口),用父类的全限定类名拼接成的key去取MapperStatement。找到了MapperStatement中后就会利用他的信息去构造SqlCommond。

MethodSignature中主要是存储的与这个方法相关的一些信息,这些信息是从invoke的入参Method以及其他的对象中取得的,这里相关的详细信息就不再做介绍。但是在MapperMethod中还有一个paramNameResolver,他用于解析入参到Sql语句,在他们之间做一个装换。

public static class MethodSignature {

    private final boolean returnsMany;                  // 判断返回类型是集合或者数组吗
    private final boolean returnsMap;                   // 判断返回类型是Map类型吗
    private final boolean returnsVoid;                  // 判断返回类型是集void吗
    private final boolean returnsCursor;                // 判断返回类型是Cursor类型吗
    private final Class<?> returnType;                  // 方法返回类型
    private final String mapKey;                        // 获得@MapKey注解里面的value值
    private final Integer resultHandlerIndex;           // 入参为ResultHandler类型的下标号
    private final Integer rowBoundsIndex;               // 入参为RowBounds类型的下标号
    private final ParamNameResolver paramNameResolver;  // 入参名称解析器
}    

3、SQL指令跳转

有了上面的MapperMethod这些基本的信息我们概要指导接下来想要实现我们自定的dao层方法应该调用Mybatis中的那些模块,这也就是SQL指令条状的用处。在决定具体要走哪一条路之后将会利用MethodSingature中的入参名称解析器来进行一个入参解析。这里面的解析结果就像一个是个原来只有一种对应关系现在经过解析之后就多映射了一个关系,而在xml中我们可以根据自己的需要选自自己需要的暗中映射关系。这些映射关系key是系统自定定义的一个入参标识符(类似于索引)或者说是我们通过@Params来指定的参数名称,这些映射关系都被保存在一个map的数据结构中。在这里的描述中我们是以一个单数据查询为示例的,在Mybatis的SqlSession中调用的方法是selectOne,但是selectOne的方法复用的实际上是selectList

4、缓存处理

为了提高性能与效率缓存的使用是必然的,在Mybatis中也涉及了二级缓存,以及缓存以及二级缓存使用的key都是相同的,都是通过:

CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

固定的算法生成,在我们的产寻中首先会对Mybatis的二级缓存进行查询,二级缓存应该是属于事务类型的缓存他的整个缓存结构应该一个是双层的Map:

List<E> list = (List<E>) tcm.getObject(cache, key);
public class TransactionalCacheManager {
  private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
}    

总的一个实现思路就是首先他会根据Cache来获取一个与事务有关的缓存,之后再根据数据库操作的key来获取具体的数据,其中事务的这一层面如果第一步中没有他会进行创建:

private TransactionalCache getTransactionalCache(Cache cache) {
  TransactionalCache txCache = transactionalCaches.get(cache);
  if (txCache == null) {
    txCache = new TransactionalCache(cache);
    transactionalCaches.put(cache, txCache);
  }
  return txCache;
}

二级缓存默认是不开启的如果需要开启需要在XML中加以配置,如果二级缓存命中那么将会开始一级缓存,一级缓存默认是开启的如果要是需要关闭则需要在setting标签中进行相关的配置。一级缓存的使用相对简单他只有一层Map结构,如果没有读取到那么则直接调用JDBC进行数据库层面的操作。根据查询到的值会对一级缓存进行维护更新,之后是对二级缓存的维护更新。在一级缓存中存在一个变量queryStack,他会记录当前的操作数,结合这个操作数以及以他的相关配置来决定是否要对缓存进行清除(不过里面的延迟加载处理倒确实不知道是干什么用的):

// eg1: configuration.getLocalCacheScope()=SESSION
/** 如果设置了<setting name="localCacheScope" value="STATEMENT"/>,则会每次执行完清空缓存。即:使得一级缓存失效 */
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
}

上面主要描述了两种缓存以及两种缓存在使用上的一个区分,在开始缓存前还有一个操作就是需要一个BoundSql(他将会query入参的一部分),这个数据结构中包含了我们是sql语句的一些关键信息:

BoundSql boundSql = ms.getBoundSql(parameterObject);
public class BoundSql {
    // 我们书写在映射器里面的一条SQL
    private String sql;
    // ParameterMapping对象会描述我们的参数,参数包含属性、名称、表达式、javaType、jdbcType、typeHandler等重要信息
    private List<ParameterMapping> parameterMappings;
    // SQL的传参对象(简单对象、POJO、Map或@Param注释的参数)
    private Object parameterObject;
    private Map<String, Object> additionalParameters;
    private MetaObject metaParameters;
}   

5.SQL语句执行

sql数据库层面的查询的入口是下面一行的代码,整体来说他的这个命名也挺有意思的哈:

queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

数据的查询中实际上使用的是jdbc的接口,其中也会包含有连接关闭等一系列的操作,这里我们在重新的回顾一下jdbc对数据库的操作有那几步:

  • 注册驱动
  • 根据驱动获取连接
  • 通过connection获取statement(包括绑定SQL以及入参绑定)
  • 利用statement执行对应的sql
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
    // eg1: sql="select id, name, age from tb_user where id = ?"
    String sql = boundSql.getSql();

    // eg1: mappedStatement.getKeyGenerator=NoKeyGenerator
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = mappedStatement.getKeyColumns();
        if (keyColumnNames == null) {
            return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
        } else {
            return connection.prepareStatement(sql, keyColumnNames);
        }
    }
    // eg1: mappedStatement.getResultSetType() = null
    else if (mappedStatement.getResultSetType() != null) {
        return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(),
                ResultSet.CONCUR_READ_ONLY);
    } else {
        // eg1: sql="select id, name, age from tb_user where id = ?"
        /** 准备预编译语句 */
        return connection.prepareStatement(sql);
    }
}

上面展示的是Mybatis对于statement对象获取相关的源码,这里面我们就可以直接的看到JDBC的影子了

6、结果集处理

对结果集的处理是在底层SQL执行完之后就直接执行的,这样也是可以理解的越是在内部后边就可以避免重复处理了。对于结果的类型是可以有多种的——对象类型、Map或是鉴别器,以及是否包含关联语句这些因素都是需要在结果集处理的模块中需要考虑的。Mybatis中自带分页的实现也是在结果及处理模块中实现的,这里应该联想到数据库层面实现以及他的优化策略——利用子查询确定想要读取的数据范围,这样避免占用大量的Buffer Pool

// eg1: skipRows里面没做什么事情
/** 将指针移动到rowBounds.getOffset()指定的行号,即:略过(skip)offset之前的行 */
skipRows(rsw.getResultSet(), rowBounds);

三、其他关键点

1、四大处理组件

  • Executor

[外链图片转存中...(img-VKmBuSRt-1647747183259)]

  • StatementHandler:生成JDBC中的Statement

  • ParameterHandler:参数的解析与绑定

  • ResultHandler:结果集的处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值