Mybatis源码学习(十二):拦截器(结束篇)

一、前文回顾

在前几篇文章中我们学习了Mybatis中的一级二级缓存。今天将要开始一个新的模块也是本系列的最后一个模块了,拦截器。

二、拦截器和过滤器

提到拦截器我们会很自然的联想到和他功能很相似的过滤器,在讲解Mybatis拦截器之前,我们有必要先简单的比较一下这两者。

  • 过滤器(Filter):它依赖于servlet容器。在实现上,基于函数回调,它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的,是用来做一些过滤操作,获取我们想要获取的数据,比如:在Javaweb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。通常用的场景是:在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等
  • 拦截器(Interceptor):它不依赖于Servlet框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

①拦截器是基于java的反射机制的,而过滤器是基于函数回调。
②拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
③拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
④拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
⑤在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。

⑥拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
参考文章:https://www.cnblogs.com/panxuejun/p/7715917.html

三、Mybatis中的拦截器

1、分页插件原理

经常使用Mybatis的你一定使用过Mybatis的分页插件,我们本身在SQL中并没有写任何关于分页的信息,但是最终的执行结果却分了页;那这是怎么实现的呢?没错就是分页插件,例如 PageHelper,实现原理也很简单就是通过Mybatis自带的拦截器,拦截我们要的查询,通过改写SQL来实现分页。

2、如何使用Mybatis拦截器

在Mybatis中使用拦截器本身并不复杂,基本上可以分为以下几个步骤。
1、定义一个拦截器类并实现Mybatis的拦截器接口
org.apache.ibatis.plugin
2、在拦截器中配置具体要拦截的方法,可以是一个也可以是多个
3、在配置文件中添加我们自定义的拦截器

3、自定义拦截器Demo

Mybatis源码学习(十二):拦截器(结束篇)_第1张图片
代码分析:首先实现interceptor接口没太多好说的,并且重写接口中定义的三个方法,分别是:
intercept:执行拦截方法,通过Invocation对象,可以拿到被拦截的对象、参数、方法等。
plugin:插件(这个翻译感觉不太好),一般直接返回即可(可以理解为固定写法)。
setProperties:获取当前拦截器的Properties,这个Properties可以在配置文件的节点下定义
然后需要编写我们要拦截的方法,这里我们拦截Executor,当然了也可以拦截其他的

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))

Mybatis源码学习(十二):拦截器(结束篇)_第2张图片
运行一下看看结果,可以看到我们通过拦截器拦截到了查询方法。Mybatis源码学习(十二):拦截器(结束篇)_第3张图片

4、PageHelper源码分析

package com.github.pagehelper;
import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.ExecutorUtil;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
 * Mybatis - 通用分页拦截器
 * 

* GitHub: https://github.com/pagehelper/Mybatis-PageHelper *

* Gitee : https://gitee.com/free/Mybatis_PageHelper * * @author liuzh/abel533/isea533 * @version 5.0.0 */ @ SuppressWarnings( { "rawtypes", "unchecked" })@ Intercepts( {@ Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }), }) public class PageInterceptor implements Interceptor { private volatile Dialect dialect; private String countSuffix = "_COUNT"; protected Cache < String, MappedStatement > msCountMap = null; private String default_dialect_class = "com.github.pagehelper.PageHelper";@ Override public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //由于逻辑关系,只会进入一次 if(args.length == 4) { //4 个参数时 boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 个参数时 cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } checkDialectExists(); List resultList; //调用方法判断是否需要进行分页,如果不需要,直接返回结果 if(!dialect.skip(ms, parameter, rowBounds)) { //判断是否需要进行 count 查询 if(dialect.beforeCount(ms, parameter, rowBounds)) { //查询总数 Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql); //处理查询总数,返回 true 时继续分页查询,false 时直接返回 if(!dialect.afterCount(count, parameter, rowBounds)) { //当查询总数为 0 时,直接返回空的结果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { if(dialect != null) { dialect.afterAll(); } } } /** * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化 *

* 因此这里会出现 null 的情况 fixed #26 */ private void checkDialectExists() { if(dialect == null) { synchronized(default_dialect_class) { if(dialect == null) { setProperties(new Properties()); } } } } private Long count(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { String countMsId = ms.getId() + countSuffix; Long count; //先判断是否存在手写的 count 查询 MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if(countMs != null) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { countMs = msCountMap.get(countMsId); //自动创建 if(countMs == null) { //根据当前的 ms 创建一个返回值为 Long 类型的 ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); msCountMap.put(countMsId, countMs); } count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler); } return count; }@ Override public Object plugin(Object target) { return Plugin.wrap(target, this); }@ Override public void setProperties(Properties properties) { //缓存 count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); String dialectClass = properties.getProperty("dialect"); if(StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } try { Class <? > aClass = Class.forName(dialectClass); dialect = (Dialect) aClass.newInstance(); } catch(Exception e) { throw new PageException(e); } dialect.setProperties(properties); String countSuffix = properties.getProperty("countSuffix"); if(StringUtil.isNotEmpty(countSuffix)) { this.countSuffix = countSuffix; } } }

源码分析:
1、和我们之前说的一样,PageInterceptor也实现了Interceptor接口,并且拦截的是Executor类中的query方法(毕竟分页只和查询有关)
Mybatis源码学习(十二):拦截器(结束篇)_第4张图片
2、核心方法
Mybatis源码学习(十二):拦截器(结束篇)_第5张图片
1、首先Mybatis会从invocation对象中拿到MappedStatment、parameter(sql参数)、RowBounds(分页相关参数)、ResultHandler(结果处理器)。
2、判断是否需要进行分页,如果不分页则直接返回结果集。如果需要分页则会先统计总数。(这也就是为什么我们能在控制台中看到 select count(0) from xxxxxxx),如果数量为0则不需要再查查询直接返回空集。
这里就是一个优化点,我们可以重写count方法,具体的方法:例如当前的分页查询方法为 List listName(),我们可以重写count方法,List listName_Count(),这样就会覆盖Mybatis自带的实现。
3、当满足了分页条件后,则会执行分页查询,这里的注释已经比较清晰,也好理解所以不再赘述。
Mybatis源码学习(十二):拦截器(结束篇)_第6张图片

四、结束语

至此Mybatis这系列的文章已经结束,我们从开始如何使用Mybatis到Mybtis核心对象分析、Mybatis流程Debug、再到缓存、拦截器。个人认为对于Mybatis的学习已经够用了,当然学无止境我也会继续探究。接下来的文章会讲述其他的开源框架,计划是Spring、Spring MVC、Springboot、Netty。如果有兴趣的小伙伴可以关注一下,希望对你有所帮助。

未完待续

你可能感兴趣的:(Mybatis源码学习,mybatis,学习,java)