MyBatis源码分析のSql执行流程
文章目录
- 前言
- 一、准备工作
- 1.1、newExecutor
- 二、执行Sql
- 2.1、getMappedStatement
- 2.2、query
- 三、Cache装饰器的执行时机
- 四、补充
- 总结
前言
本篇主要介绍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前的准备工作:
首先会去获取默认的执行器类型:
执行器类型是一个枚举类:
- SIMPLE是默认执行器,每执行一次 SQL,就创建一个新的 Statement 对象。
- REUSE是可重用执行器,如果执行相同 SQL(完全一样的 SQL 语句,包括查询条件,参数),会复用之前缓存的Statement。
- BATCH是批处理执行器,支持批量执行多个 SQL,一起发送给数据库。
可以在配置文件中配置:
<configuration>
<settings>
<!-- 设置默认执行器类型:SIMPLE / REUSE / BATCH -->
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
</configuration>
在获取到执行器类型后,就会进入openSessionFromDataSource
方法,开启Session,在这里传入的事务隔离级别默认为null,是否手动提交事务为false(默认不会自动提交):
在 **tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);这一行代码中,会去创建事务,默认的是原生的JDBC事务:
在final Executor executor = configuration.newExecutor(tx, execType);**这一行代码,会跳转到newExecutor方法。
1.1、newExecutor
在newExecutor
方法中,主要做了三件事:
- 实例化具体的
Executor
对象。 - 使用装饰器模式,对
Executor
进行包装。 - 处理插件的情况。
实例化具体的Executor
对象。
案例中默认的是simple
还会检查是否开启了二级缓存
(是否真正启用二级缓存 = cacheEnabled 为 true + Mapper 中配置了 cache标签,cacheEnabled 是默认为true的)。
这里同样使用了装饰器模式,创建CachingExecutor
实例时,将当前的Executor
作为参数赋值给CachingExecutor
的delegate
属性,而当前的Executor
的实际类型是SimpleExecutor。
最终的效果是
CachingExecutor
中持有了一个SimpleExecutor
:
SimpleExecutor
、ReuseExecutor
、BatchExecutor
都继承自一个公共的抽象类BaseExecutor
,并且实现了Executor
顶级接口。而CachingExecutor
同样也实现了Executor
顶级接口。
SimpleExecutor/ReuseExecutor/BatchExecutor 专注于 SQL 的执行逻辑;而CachingExecutor专注于 缓存处理(缓存命中、缓存写入、缓存失效)。装饰器的目的就是将CachingExecutor
和某个BaseExecutor
具体的子类进行聚合。
简单理解:
CachingExecutor
负责缓存。SimpleExecutor
负责真正执行数据库操作。
最后还会进行插件逻辑的处理:
什么是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
<plugins>
<plugin interceptor="com.example.MySimpleInterceptor">
<property name="logLevel" value="DEBUG"/>
</plugin>
</plugins>
进入pluginAll
方法后,首先会去遍历所有自定义的插件,然后执行它的plugin
方法:
默认的
plugin
方法:
创建动态代理
如果配置了插件,最后
newExecutor
拿到的对象是代理对象
。
最后回到openSessionFromDataSource
方法,将上一步得到的执行器,以及configuration
属性,autoCommit
标记,再次包装成DefaultSqlSession
对象返回。到这一步为止,准备工作就完成了
二、执行Sql
执行sql,本质上就是调用上一步得到的SqlSession
其中封装好底层JDBC的selectOne
(查询单条),selectList
(查询全部)等方法,这里以selectOne
为例:
传入的参数是mapper路径+查询方法名。以及查询条件
继续调用
selectList
(注意下方的分支,这里是根据条件查询单条,如果查出多条则会报错)。
继续跟踪,这里会再加上一个默认的分页参数。
执行sql查询的底层方法:
2.1、getMappedStatement
在调用执行器的query方法之前,首先会去根据参数中的路径+方法名,去解析出的xml文件的configuration
结果中查找该mapper以及方法是否存在。
这里的id就是mapper路径 + 方法名
最终找到的是该方法在mapper.xml中对应解析出的MappedStatement
2.2、query
query
会拿到上一步得到的MappedStatement
,去执行sql。此时如果设置过自定义插件,就会转到Plugin
的invoke
方法,首先查找这个方法是不是被声明拦截过:
符合条件,则会进入目标自定义拦截器的intercept方法,执行自定义的逻辑和目标方法:
然后执行目标方法,因为当前的
Executor
类型是CachingExecutor
,所以进入它的query
方法,在query
方法中,同样做了三件事:
- 根据参数生成对应的 SQL(带 ? 占位符)和参数映射。
- 构建缓存键 CacheKey,用于标识这个 SQL 和参数是否在一级/二级缓存中已有结果。
- 调用主查询方法。
getBoundSql
方法中,实际会调用sqlSource
的getBoundSql
,会根据动态和静态sql,走不同的实现类:
最终会得到:
然后在**CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);**这一行代码所调用的方法中,完成缓存key的设置:
会根据类名 + 方法名 + sql + 分页等参数生成一个全局唯一的缓存key
最后在query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);代码中执行sql的操作:
首先会去判断是否开启了二级缓存
,如果开启了,就能拿到在解析mapper.xml时构建的装饰器对象:(包装完成的层次:SynchronizedCache线程同步缓存区->LoggingCache统计命中率以及打印日志->SerializedCache序列化->LruCache最少使用->PerpetualCache默认。)会进入if分支,经过处理后,走 一、准备工作
中创建的具体执行器。
如果获取不到二级缓存,就直接走一、准备工作
中创建的具体执行器。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取当前 MappedStatement 所对应的二级缓存对象(如果 mapper.xml 中配置了 <cache/>)
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<E> list = (List<E>) 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,并且在执行完成后,对返回值进行解析和封装。
三、Cache装饰器的执行时机
当执行session.commit();时,会从SynchronizedCache
开始,逐个向下调用putObject方法,这里体现的是责任链模式
如果开启二级缓存,sql的返回值又需要封装成对象,则该对象必须实现序列化接口,否则在
SerializedCache
这一个cache会抛出异常。
责任链执行完成。
四、补充
上面案例中的执行sql方式,使用的是
User user = session.selectOne("com.example.mybatis.mapper.UserMapper.selectById", 1);
也可以通过下面的方式。
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.selectById(1);
通过这种方式,是创建目标Mapper接口的动态代理。
然后在执行mapper中对应方法时,会被代理到
MapperProxy
的invoke
方法:
在execute方法中对于增删改查的sql进行分派,后续的操作和上文中提到的类似。
总结
MyBatis源码中,一条sql的执行,分为两大阶段:
- 准备阶段:体现是openSession方法,首先创建事务对象(默认的是JDBC的事务),创建执行器,并通过装饰器模式,包装到
CachingExecutor
中。还会进行插件逻辑的处理。如果有插件,就会创建CachingExecutor
代理对象并返回,否则返回普通的CachingExecutor
对象。最终会将CachingExecutor
对象和解析xml得到的configuration对象一起封装成SqlSession
对象。 - 执行阶段:根据参数生成对应的 SQL(带 ? 占位符)和参数映射,构建缓存键 CacheKey,用于标识这个 SQL 和参数是否在一级/二级缓存中已有结果,最后会判断是否配置了二级缓存:
- 配置了, 从事务缓存管理器中尝试获取缓存数据:
- 获取不到,查询数据库。
- 获取到,返回结果。
- 没有配置,直接查询数据库。
- 配置了, 从事务缓存管理器中尝试获取缓存数据:
在事务提交,调用commit方法时,才会从SynchronizedCache
开始去执行责任链,放入二级缓存。
四大拦截器的执行时机:
- Executor是在执行query时触发,上面已经提到过。
- StatementHandler 在执行prepare时触发,是在执行sql语句前的预处理阶段。
- ParameterHandler 是在预处理阶段的准备参数时触发:
- ResultSetHandler 是在sql执行完成,处理返回值结果的时候触发: