目录

MyBatis源码分析Sql执行流程

MyBatis源码分析のSql执行流程


前言

本篇主要介绍MyBatis解析配置文件完成后,执行sql的相关逻辑: public class Main { public static void main(String[] args) throws Exception { //将xml构筑成configuration配置类 Reader reader = Resources.getResourceAsReader(“mybatis-config.xml”); //解析xml,注册成SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); try (SqlSession session = sqlSessionFactory.openSession()) { User user = session.selectOne(“com.example.mybatis.mapper.UserMapper.selectById”, 1); System.out.println(user); } } } 在上面的案例中,真正执行sql的核心是**User user = session.selectOne(“com.example.mybatis.mapper.UserMapper.selectById”, 1);**这一行代码。

一、准备工作

**sqlSessionFactory.openSession()**这一行代码,是执行sql前的准备工作: https://i-blog.csdnimg.cn/direct/b480ca5a92684e5997ca9e4c57a748b9.png 首先会去获取默认的执行器类型: https://i-blog.csdnimg.cn/direct/ffc5bc2e99d4453d8702ab5c6301a429.png 执行器类型是一个枚举类:

  • SIMPLE是默认执行器,每执行一次 SQL,就创建一个新的 Statement 对象。
  • REUSE是可重用执行器,如果执行相同 SQL(完全一样的 SQL 语句,包括查询条件,参数),会复用之前缓存的Statement。
  • BATCH是批处理执行器,支持批量执行多个 SQL,一起发送给数据库。 https://i-blog.csdnimg.cn/direct/64ed0a76f17244f188d335f4e38e59b9.png 可以在配置文件中配置:

在获取到执行器类型后,就会进入openSessionFromDataSource方法,开启Session,在这里传入的事务隔离级别默认为null,是否手动提交事务为false(默认不会自动提交): https://i-blog.csdnimg.cn/direct/6580678e1484443e856cb68a4403675c.png 在 **tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);这一行代码中,会去创建事务,默认的是原生的JDBC事务: https://i-blog.csdnimg.cn/direct/dd3d6610dcd743c5966ab9611fae7b2e.pngfinal Executor executor = configuration.newExecutor(tx, execType);**这一行代码,会跳转到newExecutor方法。

1.1、newExecutor

newExecutor方法中,主要做了三件事:

  1. 实例化具体的Executor对象。
  2. 使用装饰器模式,对Executor进行包装。
  3. 处理插件的情况。 实例化具体的Executor对象。 https://i-blog.csdnimg.cn/direct/39f5de23092447d8b7d5fe183ff07845.png案例中默认的是simple 还会检查是否开启了二级缓存(是否真正启用二级缓存 = cacheEnabled 为 true + Mapper 中配置了 cache标签,cacheEnabled 是默认为true的)。 这里同样使用了装饰器模式 ,创建CachingExecutor实例时,将当前的Executor作为参数赋值给CachingExecutordelegate属性,而当前的Executor的实际类型是SimpleExecutor。 https://i-blog.csdnimg.cn/direct/5e7de5c3ffda4065911171ae22be486c.png 最终的效果是CachingExecutor中持有了一个SimpleExecutorhttps://i-blog.csdnimg.cn/direct/9ab4a05c58734e89aa52ff3fd9fb7dc2.png SimpleExecutorReuseExecutorBatchExecutor都继承自一个公共的抽象类BaseExecutor,并且实现了Executor顶级接口。而CachingExecutor同样也实现了Executor顶级接口。 SimpleExecutor/ReuseExecutor/BatchExecutor 专注于 SQL 的执行逻辑;而CachingExecutor专注于 缓存处理(缓存命中、缓存写入、缓存失效)。装饰器的目的就是将CachingExecutor和某个BaseExecutor具体的子类进行聚合。 简单理解:
  • CachingExecutor 负责缓存。
  • SimpleExecutor 负责真正执行数据库操作。 https://i-blog.csdnimg.cn/direct/712aea64e2a64a7ab0ad5cd39cc8deeb.png 最后还会进行插件逻辑的处理: https://i-blog.csdnimg.cn/direct/c27dc1f16a1f43539e84257fd12c0bbf.png 什么是MyBatis的插件?简单来说,是通过动态代理机制,对MyBatis内部行为进行拦截,MyBatis只允许拦截以下四种类型的核心接口方法(常见的分页插件就是基于此实现):
    类型接口常见场景
    Executor执行器拦截sql,做 SQL 日志、分页
    ParameterHandler参数处理器拦截参数设置,修改参数值
    ResultSetHandler结果集处理器修改结果集,比如字段映射处理
    StatementHandler语句处理器修改 SQL 语句,如 SQL 重写、SQL 审计
    如果需要使用插件,需要进行配置,先自定义一个插件类:
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.mapping.MappedStatement;
    import java.util.Properties;
    @Intercepts({
    @Signature(
    type = Executor.class, // 拦截 Executor
    method = “query”, // 方法名
    args = {MappedStatement.class, Object.class, org.apache.ibatis.session.RowBounds.class, ResultHandler.class}
    )
    })
    public class MySimpleInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    System.out.println("»> MyBatis 插件开始拦截:方法 = " + invocation.getMethod().getName());
    Object result = invocation.proceed(); // 执行原方法
    System.out.println("»> MyBatis 插件结束拦截");
    return result;
    }
    @Override
    public void setProperties(Properties properties) {
    // 可以在 XML 中配置插件参数
    System.out.println("»> 插件参数:" + properties);
    }
    }
    然后在xml中进行设置,注意:MyBatis 对configuration内部各个子元素的顺序有严格要求

configuration properties settings typeAliases typeHandlers objectFactory objectWrapperFactory reflectorFactory plugins ← 插件必须在这之前出现 environments databaseIdProvider mappers

进入pluginAll方法后,首先会去遍历所有自定义的插件,然后执行它的plugin方法: https://i-blog.csdnimg.cn/direct/9ccc2fe053da475abd73394bfa8f7d9c.png 默认的plugin方法: https://i-blog.csdnimg.cn/direct/25d4197156b84f719cefb2c6bc64e3fd.png 创建动态代理 https://i-blog.csdnimg.cn/direct/e6da9e1c25bc47359bbea72f35fd238f.png 如果配置了插件,最后newExecutor拿到的对象是代理对象https://i-blog.csdnimg.cn/direct/733ea953b2a04d6a9aa4a509ece14c11.png 最后回到openSessionFromDataSource方法,将上一步得到的执行器,以及configuration属性,autoCommit标记,再次包装成DefaultSqlSession对象返回。到这一步为止,准备工作就完成了 https://i-blog.csdnimg.cn/direct/ece7c1a5c29340ca9f10dc5fce8e1908.png

二、执行Sql

执行sql,本质上就是调用上一步得到的SqlSession其中封装好底层JDBC的selectOne(查询单条),selectList(查询全部)等方法,这里以selectOne为例: 传入的参数是mapper路径+查询方法名。以及查询条件 https://i-blog.csdnimg.cn/direct/80d157c2c5384d15b7e1684d363cf72e.png 继续调用selectList(注意下方的分支,这里是根据条件查询单条,如果查出多条则会报错)。 https://i-blog.csdnimg.cn/direct/5b301d83af684996b071935b083c119c.png 继续跟踪,这里会再加上一个默认的分页参数。 https://i-blog.csdnimg.cn/direct/8afc37b2a2a1422ebb0810ba9ed6771e.png 执行sql查询的底层方法: https://i-blog.csdnimg.cn/direct/29f5e3e6fdcd4f48b3331005cdf443da.png

2.1、getMappedStatement

在调用执行器的query方法之前,首先会去根据参数中的路径+方法名,去解析出的xml文件的configuration结果中查找该mapper以及方法是否存在。 https://i-blog.csdnimg.cn/direct/6ec0a282f25e473f8afdf876d97c2a6f.png_这里的id就是mapper路径

  • 方法名_ https://i-blog.csdnimg.cn/direct/b546be656b174806bb9a3abc38ff3745.png最终找到的是该方法在mapper.xml中对应解析出的MappedStatement

2.2、query

query会拿到上一步得到的MappedStatement,去执行sql。此时如果设置过自定义插件,就会转到Plugininvoke方法,首先查找这个方法是不是被声明拦截过: https://i-blog.csdnimg.cn/direct/b8b6c74fc1dd42158b0a91328cbb8e74.png https://i-blog.csdnimg.cn/direct/a080f216031e47cba08a1918544bbee4.png 符合条件,则会进入目标自定义拦截器的intercept方法,执行自定义的逻辑和目标方法: https://i-blog.csdnimg.cn/direct/a9135e7158ad4f789b02e5ad24400c28.png 然后执行目标方法,因为当前的Executor类型是CachingExecutor,所以进入它的query方法,在query方法中,同样做了三件事:

  1. 根据参数生成对应的 SQL(带 ? 占位符)和参数映射。
  2. 构建缓存键 CacheKey,用于标识这个 SQL 和参数是否在一级/二级缓存中已有结果。
  3. 调用主查询方法。 https://i-blog.csdnimg.cn/direct/035f38e7f688471593090209e2f11e80.png getBoundSql方法中,实际会调用sqlSourcegetBoundSql,会根据动态和静态sql,走不同的实现类: https://i-blog.csdnimg.cn/direct/33957172f2ed4908a19475b1543dbcb1.png 最终会得到: https://i-blog.csdnimg.cn/direct/56c42b8d03354a81b8bc6354d18126a6.png 然后在**CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);**这一行代码所调用的方法中,完成缓存key的设置: https://i-blog.csdnimg.cn/direct/2b12efd5f2464b63a33215d101f8cdcc.png_会根据类名
  • 方法名 + sql + 分页等参数生成一个全局唯一的缓存key_ 最后在query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);代码中执行sql的操作: 首先会去判断是否开启了二级缓存,如果开启了,就能拿到在解析mapper.xml时构建的装饰器对象:(包装完成的层次:SynchronizedCache线程同步缓存区->LoggingCache统计命中率以及打印日志->SerializedCache序列化->LruCache最少使用->PerpetualCache默认。)会进入if分支,经过处理后,走 一、准备工作 中创建的具体执行器。 如果获取不到二级缓存,就直接走一、准备工作 中创建的具体执行器。 @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 获取当前 MappedStatement 所对应的二级缓存对象(如果 mapper.xml 中配置了 ) Cache cache = ms.getCache(); // 判断是否配置了二级缓存 if (cache != null) { // 判断是否需要在本次查询前刷新缓存(一般是增删改操作,或者某些查询配置了 flushCache=true) flushCacheIfRequired(ms); // 如果当前查询语句允许使用缓存(useCache=true),且没有自定义的 ResultHandler // ➜ 自定义 ResultHandler 无法缓存结果 if (ms.isUseCache() && resultHandler == null) { // 确保当前 SQL 中没有 OUT 参数(例如存储过程),否则不能缓存 ensureNoOutParams(ms, boundSql); // 从事务缓存管理器中尝试获取缓存数据 @SuppressWarnings(“unchecked”) List list = (List) tcm.getObject(cache, key); // 如果缓存未命中,则执行真正的数据库查询 if (list == null) { // 通过内部真正的执行器(delegate)查询数据库 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 将查询结果放入缓存(注意:此时只是放入事务缓存,等事务提交后才真正写入二级缓存) tcm.putObject(cache, key, list); } // 返回缓存中的数据或数据库查询结果 return list; } } // 如果没有配置缓存,或不满足缓存使用条件,直接走真实执行器查询数据库 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 执行query方法,最终底层调用的是JDBC的execute,并且在执行完成后,对返回值进行解析和封装。 https://i-blog.csdnimg.cn/direct/2f2989f837ac4201b12b89347e2ec6a9.pnghttps://i-blog.csdnimg.cn/direct/510724929fc34581bd2bfc3a586093ff.png

三、Cache装饰器的执行时机

当执行session.commit();时,会从SynchronizedCache开始,逐个向下调用putObject方法,这里体现的是责任链模式 https://i-blog.csdnimg.cn/direct/2b8861edb9024da48e580efd47474cab.png 如果开启二级缓存,sql的返回值又需要封装成对象,则该对象必须实现序列化接口,否则在SerializedCache这一个cache会抛出异常。 https://i-blog.csdnimg.cn/direct/a7e8b8915ca844dcbdb75bf4589db95d.png 责任链 执行完成。 https://i-blog.csdnimg.cn/direct/822eb4d82b3c4a5b9cca353c13427019.png

四、补充

上面案例中的执行sql方式,使用的是 User user = session.selectOne(“com.example.mybatis.mapper.UserMapper.selectById”, 1); 也可以通过下面的方式。 UserMapper mapper = session.getMapper(UserMapper.class); mapper.selectById(1); 通过这种方式,是创建目标Mapper接口的动态代理。 https://i-blog.csdnimg.cn/direct/4d2662f9d7ee4b5cbea5d5c75b1e4b1d.png 然后在执行mapper中对应方法时,会被代理到MapperProxyinvoke方法: https://i-blog.csdnimg.cn/direct/6d767e56ddc0437bbcdf59ab132486af.png https://i-blog.csdnimg.cn/direct/ea60ff730fba4826be2e1bf0ce76a0d8.png 在execute方法中对于增删改查的sql进行分派,后续的操作和上文中提到的类似。 https://i-blog.csdnimg.cn/direct/7d5c22e1e3544f48b27acec30261273d.png

总结

MyBatis源码中,一条sql的执行,分为两大阶段:

  1. 准备阶段:体现是openSession 方法,首先创建事务对象(默认的是JDBC的事务),创建执行器,并通过装饰器模式 ,包装到CachingExecutor中。还会进行插件逻辑的处理。如果有插件,就会创建CachingExecutor代理对象并返回,否则返回普通的CachingExecutor对象。最终会将CachingExecutor对象和解析xml得到的configuration对象一起封装成SqlSession对象。
  2. 执行阶段:根据参数生成对应的 SQL(带 ? 占位符)和参数映射,构建缓存键 CacheKey,用于标识这个 SQL 和参数是否在一级/二级缓存中已有结果,最后会判断是否配置了二级缓存:
  • 配置了, 从事务缓存管理器中尝试获取缓存数据:
  • 获取不到,查询数据库。
  • 获取到,返回结果。
  • 没有配置,直接查询数据库。 在事务提交,调用commit方法时,才会从SynchronizedCache开始去执行责任链,放入二级缓存。 四大拦截器的执行时机:
  • Executor是在执行query时触发,上面已经提到过。
  • StatementHandler 在执行prepare时触发,是在执行sql语句前的预处理阶段。 https://i-blog.csdnimg.cn/direct/e3d50edd68f94889bbff0c66a79a2889.png https://i-blog.csdnimg.cn/direct/d15094e2217e484695c9d68bb335eb12.png
  • ParameterHandler 是在预处理阶段的准备参数时触发: https://i-blog.csdnimg.cn/direct/fd373ba326654c20932cbcedbf8f8f84.png
  • ResultSetHandler 是在sql执行完成,处理返回值结果的时候触发: https://i-blog.csdnimg.cn/direct/7f852b1b1580461b85afc9394c527735.png