
mybatis源码解析一(整体了解) 精华
观看的前提:你使用过MyBatis。
一、MyBatis是干什么的?
抛开自身的定义不说。其本质就是操作数据库的,因为是在java写的,所以逃不脱底层使用java的JDBC,通过实现相关底层接口调用mysql oracle等相关数据库驱动,从而实现数据库的各种功能,我们研究源码就是看本质。其关系如下图:
二、MyBatis 如何操作数据库?
2.1JDBC执行流程
2.2mybatis 执行流程
其执行过程堆栈如下图:
• 方法代理:与MyBatis交互的门面,存在的目的是为了方便调用,本身不会影响执行逻辑。(也就是说可以直接去掉,只是调用会麻烦些)
• 会话:与MyBatis交互的门面,所有对数据库操作必须经过它,但它不会真正去执行业务逻辑,而是交给Execute。另外他不是线程安全的所以不能跨线程调用。
• 执行器:真正执行业务逻辑的组件,其具体职能包括与JDBC交互,缓存管理、事物管理等。
2.3举例说明
以上过程可以类比成你去饭店点餐。
1、由服务员(SqlSession 门面设计模式)为你下单。
2、交给后厨(Execute 责任链、模板方法设计模式 )制作。
3、后厨他们分工合作职责分明,有做中餐的(SimpleExecutor)、有洗菜的(BaseExecutor 共性处理)。
4、还有负责打包整理端盘的(ResultHandler 数据库行数据转换成java对象处理器)。
5、最后在由服务员交到你手上。服务员不能同时为两个顾客点单(SqlSession 不能同时为两个线程共用)。
这个方法代理MapperMethod 怎么表示呢?这就是叫外卖呀!你的点餐过程被外卖公司代理了,但最终还是要由服务员接单,后厨制作。(屏蔽底层实现)
当然这里还有很多细节(配置解析、缓存实现、线程安全、sql解析、参数解析、结果解析等),后续会结合源码去一一讲解。而你现在需要记住的是总体的执行过程即:方法代理–>会话–>执行器–>JDBC
2.4mybatis涉及到类总体标注
三、mybatis扩展插件原理
在编写 mybatis 插件的时候,首先要实现 Interceptor 接口,如果是使用spring体系 可以通过@Intercepts方式实现下面会有单独的介绍、也可以通过在配置文件 mybatis-conf.xml 中添加插件下列配置的方式实现
这里需要注意的是,添加的插件是有顺序的,因为在解析的时候是依次放入 ArrayList 里面,而调用的时候其顺序为:2 > 1 > target > 1 > 2;(插件的顺序可能会影响执行的流程)更加细致的讲解可以参考 QueryInterceptor 规范;
然后当插件初始化完成之后,添加插件的流程如下:
首先要注意的是,mybatis 插件的拦截目标有四个,Executor、StatementHandler、ParameterHandler、ResultSetHandler:
这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象; 其责任链的添加过程如下:
最终动态代理生成和调用的过程都在 Plugin 类中:
这里所说的签名是指在编写插件的时候,指定的目标接口和方法,例如:
这里就指定了拦截 Executor 的具有相应方法的 update、query 方法;注解的代码很简单,大家可以自行查看;然后通过 getSignatureMap 方法反射取出对应的 Method 对象,在通过 getAllInterfaces 方法判断,目标对象是否有对应的方法,有就生成代理对象,没有就直接反对目标对象;
在调用的时候:
3.1、PageHelper 拦截器实现分析
mybatis 插件我们平时使用最多的就是分页插件了,这里以 PageHelper 为例,其使用方法可以查看相应的文档 如何使用分页插件,因为官方文档讲解的很详细了,我这里就简单补充分页插件需要做哪几件事情;
使用:
PageHelper 还有很多中使用方式,这是最常用的一种,他其实就是在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页;(PageHelper 使用的其他方法,也是围绕 Page 对象的设置进行的)
- 首先可以看到拦截的是 Executor 的两个 query 方法(这里的两个方法具体拦截到哪一个受插件顺序影响,最终影响到 cacheKey 和 boundSql 的初始化);
- 然后使用 checkDialectExists 判断是否支持对应的数据库;
- 在分页之前需要查询总数,这里会生成相应的 sql 语句以及对应的 MappedStatement 对象,并缓存;
- 然后拼接分页查询语句,并生成相应的 MappedStatement 对象,同时缓存;
- 最后查询,查询完成后使用 dialect.afterPage 移除 Page对象
四、mybatis执行器Executro
4.1创建DefaultSqlSession
通过会话工厂 创建 DefaultSqlSession
DefaultSqlSessionFactory.openSession().openSessionFromDataSource()
方法的入参通过mybatis的配置文件中的属性设置而来 比如 ExecutorType 的定义 defaultExecutorType 是否使用二级缓存 cacheEnabled 等
4.2执行器的创建逻辑
org.apache.ibatis.session.Configuration#newExecutor() 逻辑来看都是比较简单
4.3从会话到获取数据
4.3.1判断执行的类型
我们以 查询单条的逻辑selectOne 看完后续的流程
4.3.2CachingExecutor
org.apache.ibatis.executor.CachingExecutor#query() 二级缓存的执行器 逻辑也不复杂 常见的思路无非就是拿着key去缓存里面看看是否有值,这里主要还是要考虑线程安全的情况(脏读) 如果是你 你会怎么防止脏读呢?
mybatis的实现思路和git很类似,设置了**缓存暂存区 ** 只有提交后才会把暂存区的数据刷入缓存区。
二级缓存是一个完整的缓存解决方案,和redis相关功能相似,
比如 存储的实现(内存 磁盘 自定义)
缓存满了要怎么做(各类淘汰算法 FIFO LRU 等)
线程安全等相关功能
mybatis 就针对二级缓存 设置了一个顶级接口Cache 责任链的方式调用 对这一系列进行完整的处理 也是一个扩容点。
4.3.2 BaseExecutor
org.apache.ibatis.executor.BaseExecutor#query() 经常被提及的一级缓存的实现 就是在这里了。 思考下 在开启二级缓存的情况下 是先进二级缓存还是一级缓存。 从这里 在联想操作系统cpu的多级缓存设计思想。 是不是都是异曲同工。
一级缓存总结
缓存命中条件
- SQL与参数相同
- 同一个会话
- 相同的MapperStatement ID
- RowBounds行范围相同
触发清空缓存 - 手动调用clearCache
- 执行提交回滚
- 执行update
- 配置flushCache=true
- 缓存作用域为Statement
常见面试题:MyBatis集成Spring后一级缓存失效的问题?
很多人发现,集成一级缓存后会话失效了,以为是spring Bug ,真正原因是Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,具体参见SqlSessionInterceptor。而根据前面所学,一级缓存必须是同一会话才能命中,所以在这些场景当中不能命中。
解决方案: 给Spring 添加事物 即可。添加事物之后,SqlSessionInterceptor(会话拦截器)就会去判断两次请求是否在同一事物当中,如果是就会共用同一个SqlSession会话来解决。
4.3.3 SimpleExecutor
