1、需求分析
需求:在代码层面获得Mybatis执行的SQL,修改SQL,并执行修改后的SQL
方案:Mybatis 拦截器:
注意:添加拦截器后,会拦截所有的方法
思考:其实拦截器就等同于Spring的AOP编程
细粒度:Mybatis框架中,sql最后都会交给Sqlsession执行,拦截器拦截的其实就是:
- 1、Executor执行阶段
- 2、ParameterHandler参数处理阶段
- 3、StatementHandler预编译处理阶段
- 4、ResultSetHandler结果集处理阶段
注:我们在执行Sql之前,需要先获取Mybatis的SqlSession对象,但框架的SqlSession下面还有四大对象, 所以SqlSession只是个甩手掌柜, 真正干活的却是Executor等四大对象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。
比较形象的画图比喻:
继续分析:
理论上来说,如果想拦截,这四个对象的所有方法均可拦截到
但是原则是原则,实际是实际
就像Java中的String类,它里面这么多方法,总有几个常用的
在这边主要拦截两个常用对象:
按照Mybatis执行Sql的周期来说,Executor会把sql交给StatementHandler处理,
所以我们常见的拦截方法还是StatementHandler,而且这个阶段的Sql已完成预编译,占位符出现,对Sql来说比较全面了(此时除了参数未赋值,其他都全了,就等执行了)
2、熟悉拦截器的开发结构
1、编码
类实现Intercepyor接口@Override
// 核心拦截逻辑编写
public Object intercept(Invocation invocation) throws Throwable { // 编写拦截逻辑
System.out.println("编写拦截逻辑");
// 放行该方法 return invocation.proceed(); } // 把这个拦截器的目标传给下一个拦截器 @Override public Object plugin(Object target) { // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数 if (target instanceof StatementHandler) { System.out.println("Wrapper::"); return Plugin.wrap(target, this); } else { System.out.println("pass on Wrapper::"); return target; } @Override public void setProperties(Properties properties) { //此处可以接收到配置文件的property参数,就像工作流里面的局部变量,如果业务上需要定义参数,可以通过此方法 // System.out.println(properties.getProperty("name")); }
类 标注拦截的方法和参数(防止方法重载,需参数判别方法的唯一性)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class AccessControlInterceptor implements Interceptor { }
2、配置(Configure配置)
在 mybatis-config.xml 中通过plugins 标签配置拦截器
或者利用SpringBoot快速定义@Configure标签标识Bean
思考:如何控制拦截器的执行顺序? 比如我想让拦截器x执行完之后,再执行拦截器y 参考链接:https://juejin.cn/post/6926849748481605646
好的,到这里。执行到每一个mapper文件的接口对应的sql 都会起作用了。(包括PageHelper 自动生成的Count查询,同样会被拦截)
思考:1、如何只拦截SELECT类型的sql,自动过滤掉update类型? 2、如何拦截指定的mapper接口方法? 方案:1、可以通过拦截方法的invocation参数利用反射获取本次执行sql的 MappedStatement,进而获得sqlCommandType 2、可在mapper接口方法层面添加自定义注解,同样是通过反射的方式 获得注解标志位判定应不应该进行拦截,还可通过注解传递有含义的 value
3、更细致的分析
为什么要拦截StatementHandler的prepare呢
1、Executor 会交给StatementHandler,
在StatementHandler接口实现类的方法调用中,
StatementHandler的prepare生产出的Statemen会作为参数提供给CRUD和批处理之类的操作
拦截prepare方法是一劳永逸
调用结构顺序如下图:
2、拦截prepare方法可以获得当前的Connection对象,
Connection对象是所有Java接入所有数据库的规则接口,可操作性的东西会很多
sql片段、事务,等同于JDBC 为所欲为
4、拦截器应用举例
一般什么情况下会使用拦截器:当需要操作sql时
例如:分页(添加limit sql片段),乐观锁(设置版本号校验,校验成功直接更新版本和数据,校验失败直接提示线程争抢失败请重试)
应用开发之一、pagehelper如何使用拦截器实现分页
找一个
1、com.github.pagehelper.PageInterceptor 拦截请求
第 71 - 第99行都是校验参数、创建缓存之类的操作
2、凭空捏造Count查询
第 100 行,pagehelper官方团队给出的注释是:查询总数
继续追代码,进入count(..)
先判断是否存在手写的 count 查询,
存在Count查询,用原来的 ms ,更新部分参数,直接执行并返回结果,
没有的话,根据当前的 ms 创建一个返回值为 Long 类型的 ms,并放到缓存中去
最后执行Count查询,并返回Count
接着做了一个小优化 处理查询总数,当查询总数为 0 时,直接结束查询
3、修改原始查询sql 拼接 limit进行分页
根据上一步查询来的Count,结合当前页,计算limit值,并交给Executor去执行query
至此,pagehelper整个分页粗略过程完成
4、思考使用的细节
PageHelper是如何做到只对紧跟着的第一条SQL起作用的?即使在下面添加再多的select,他仍然只对第一个select情有独钟
先揭晓答案: PageHelper.startPage在当前线程中创建一个线程变量 t1,
需要的时候就去当前线程获取 t1,使用完一次就在finally中remove掉 t1
受限于代码同步执行的特性,实现了只对第一个select起作用
(思考:若开启新线程,在主查询之前执行异步查询,Pagehelper特性会失效吗?)
源代码追溯:
(未完待续。。。)
应用开发之二、 乐观锁使用拦截器
(未完待续。。。)