前言
在正式使用拦截器之前,首先要抛出几个问题,我们平时所谈的拦截器与过滤器有什么区别?我们在使用 Mybatis 时候,如果想动态的改写 sql 如何实现?倘若在多租户的系统中,如何依据当前的线程上下文中的请求租户信息,动态的改写 sq 设置租户信息?又或者如何增加 sql 的执行耗时或者信息摘要呢?
拦截器与过滤器
- 过滤器(Filter):见名知意我们使用过滤器的目的,无非就是用来做过滤操作的。比如对请求前置过滤掉一些信息,或者前置设置某些参数,然后在从 Controller 根据这些业务特征进行对应的逻辑操作。通常用的场景是:ip 白名单、用户信息的过滤、敏感信息的过滤等等。
- 拦截器(Interceptor):从名称看来它类似于 AOP 实现了某些拦截操作,其就是在某些方法前/后,执行某些特殊的业务逻辑。而动态代理就是拦截器的简单实现,在调用方法前/后做其它业务逻辑的操作,也可以在抛出异常的时候做某些特殊的业务逻辑操作。
Mybatis 拦截器使用姿势
先不谈 Mybatis 如何实现的拦截器加载原理,我们直接上手去配置拦截器。但是为了方便下个小节查看源码,这里使用原生的拦截器配置方式,更多的配置方式,可以去查下相关资料。
首先配置拦截器分为以下几步
- 继承 org.apache.ibatis.plugin.Interceptor 接口、配置@Intercepts 与@Signature 组合注解。(详情见下个小节)
1 | /** |
- 在 mybatis-config.xml 中配置拦截器
1 | <?xml version="1.0" encoding="UTF-8" ?> |
- 验证拦截器是否生效
1 | 00:05:51.099 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter. |
Mybatis 拦截器@Intercepts 与@Signature todo
在谈这些配置之前,可以翻阅一下笔者发布的 Mybatis 核心组件的文章,以下为简要信息。在下图中,Mybatis 拦截器的拦截点有 4 种,分别为 Executor(执行器)、StatementHandler(sql 语法构建处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果处理器)。
- @Intercepts:它用于标识当前的对象是一个拦截器,其配置值是一个@Signature 的集合。
- @Signature:它用于标识需要拦截的接口、方法、对应的参数列表。
Mybatis 拦截器实现原理
上个小节提到 Mybatis 拦截器的拦截点有 4 种,但是他们是如何加载拦截并执行的呢?直接上代码
1 | // 大家经常见到的mybatis的用法 |
1 | /** |
1 | <!--mybatis-config.xml --> |
源码解析
通过此上代码,① 跟 ② 这一步没什么问题,前者就是直接根据配置文件获取输入流,后者获取
1 | // new SqlSessionFactoryBuilder().build(inputStream); |
1 | // parser.parse(); |
1 | // parseConfiguration(parser.evalNode("/configuration")); |
1 | // pluginElement(root.evalNode("plugins")); |
1 | // interceptorChain就是拦截器链,它在configuration初始化创建,其内部维护这一个空的list类型的Interceptor集合 |
1 | /** |
1 |
|
1 | public class Plugin implements InvocationHandler { |
通过此上分析,我们在执行 Executor、StatementHandler、ParameterHandler、ResultSetHandler 时的方法的时候,其实调用的都是代理对象,而代理对象通过方法增强的方式,在调用方法执行前判断是否需要拦截,并执行相关的自定义拦截逻辑的。
Mybatis 拦截点加载顺序
不知道客官有没有考虑过 Mybatis 这 4 种拦截点的加载顺序?它一定是按照组件的调用顺序拦截的吗?答案是不一定的,网上很多有说的顺序是 Executor -> ParameterHandler -> ResultHandler -> StatementHandler,其实这也不全对。
拦截点顺序验证
代码就不展示了,没什么营养,就是配置了 4 种拦截点的拦截器。验证下来,结论就是:Executor 首先执行的是没有问题的。ParameterHandler -> ResultHandler 的先后顺序也没有问题,一进一出嘛。问题就是 StatementHandler 的顺序。
- 如果你拦截的是 StatementHandler.query ()方法,那么顺序是 Executor -> ParameterHandler -> StatementHandler -> ResultHandler。
- 如果你拦截的是 StatementHandler.prepare ()方法,那么顺序是 Executor -> StatementHandler-> ParameterHandler -> ResultHandler。
源码解析
1 | // 我们在拦截Executor的query方法的时候,最终都会走到doQuery的方法逻辑中, |
1 | private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { |
从上面两段代码中不难看出,如果是拦截的 StatementHandler.prepare ()方法,那么它的执行时机是在 ParameterHandler 执行之前的,所有 StatementHandler 会先执行拦截。相反,如果是 StatementHandler.query ()方法,它要等预编译之后在进行查询,所以到这里就能说明”也不全对”的言论了。
Mybatis 拦截器应用场景
- Sql 摘要统计/监控
- 动态分页
- 多租户
- 脱敏
- 加解密
- 冗余字段新增
- …….等等
全剧终
通过本篇文章,相信你肯定会对 Mybatis 的拦截器有一个充分的认知,但文中屏蔽掉了许多细节,这些细节还需要你根据源码依照文中对应的注释去串一下自己的逻辑,也欢迎你指出文章中的错误信息,以便笔者及时改正,以免误人子弟,因排版问题,可能阅读起来不是很通顺,也欢迎来指正说明。