定制Mybatis拦截器开发【含源码追溯】

 1、需求分析

需求:在代码层面获得Mybatis执行的SQL,修改SQL,并执行修改后的SQL

方案:Mybatis 拦截器:

定制Mybatis拦截器开发【含源码追溯】

 

 

 注意:添加拦截器后,会拦截所有的方法

 思考:其实拦截器就等同于Spring的AOP编程

 细粒度:Mybatis框架中,sql最后都会交给Sqlsession执行,拦截器拦截的其实就是:

  • 1、Executor执行阶段
  • 2、ParameterHandler参数处理阶段
  • 3、StatementHandler预编译处理阶段
  • 4、ResultSetHandler结果集处理阶段 
注:我们在执行Sql之前,需要先获取Mybatis的SqlSession对象,但框架的SqlSession下面还有四大对象,
所以SqlSession只是个甩手掌柜,
真正干活的却是Executor等四大对象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。

比较形象的画图比喻:

定制Mybatis拦截器开发【含源码追溯】

 

 

 继续分析:

理论上来说,如果想拦截,这四个对象的所有方法均可拦截到

定制Mybatis拦截器开发【含源码追溯】

但是原则是原则,实际是实际

就像Java中的String类,它里面这么多方法,总有几个常用的

在这边主要拦截两个常用对象:

 定制Mybatis拦截器开发【含源码追溯】

 按照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方法是一劳永逸

定制Mybatis拦截器开发【含源码追溯】

调用结构顺序如下图:

定制Mybatis拦截器开发【含源码追溯】

 

2、拦截prepare方法可以获得当前的Connection对象,

     Connection对象是所有Java接入所有数据库的规则接口,可操作性的东西会很多

     sql片段、事务,等同于JDBC  为所欲为 

 定制Mybatis拦截器开发【含源码追溯】

 

 4、拦截器应用举例

一般什么情况下会使用拦截器:当需要操作sql时

例如:分页(添加limit sql片段),乐观锁(设置版本号校验,校验成功直接更新版本和数据,校验失败直接提示线程争抢失败请重试)

应用开发之一、pagehelper如何使用拦截器实现分页

 

找一个

   1、com.github.pagehelper.PageInterceptor 拦截请求

定制Mybatis拦截器开发【含源码追溯】

 

 

 第 71 - 第99行都是校验参数、创建缓存之类的操作

   2、凭空捏造Count查询

第 100 行,pagehelper官方团队给出的注释是:查询总数

定制Mybatis拦截器开发【含源码追溯】

 

 

 继续追代码,进入count(..)

定制Mybatis拦截器开发【含源码追溯】

 

先判断是否存在手写的 count 查询,

存在Count查询,用原来的 ms ,更新部分参数,直接执行并返回结果,

没有的话,根据当前的 ms 创建一个返回值为 Long 类型的 ms,并放到缓存中去

最后执行Count查询,并返回Count

 

接着做了一个小优化

处理查询总数,当查询总数为 0 时,直接结束查询

 

   3、修改原始查询sql 拼接 limit进行分页

根据上一步查询来的Count,结合当前页,计算limit值,并交给Executor去执行query

定制Mybatis拦截器开发【含源码追溯】

 

至此,pagehelper整个分页粗略过程完成

4、思考使用的细节

     PageHelper是如何做到只对紧跟着的第一条SQL起作用的?即使在下面添加再多的select,他仍然只对第一个select情有独钟

定制Mybatis拦截器开发【含源码追溯】

 

 

 

先揭晓答案: PageHelper.startPage在当前线程中创建一个线程变量 t1,

                   需要的时候就去当前线程获取 t1,使用完一次就在finally中remove掉 t1

                   受限于代码同步执行的特性,实现了只对第一个select起作用

                 (思考:若开启新线程,在主查询之前执行异步查询,Pagehelper特性会失效吗?)

源代码追溯:

 (未完待续。。。)

 应用开发之二、 乐观锁使用拦截器

 (未完待续。。。)

 

 

上一篇:mybatis-原理


下一篇:mybatis-插件