Mybatis拦截器源码详解
创始人
2024-01-29 10:34:07
0

Mybatis拦截器源码详解

  • Mybatis相关全览
  • 一、简介
    • 执行与添加顺序
    • 拦截器生效入口
  • 二、使用
    • 例子
  • 三、原理
    • 加载入口
    • 生成代理
      • 遍历拦截器
      • 匹配&生成代理
  • 四、实践例子

本文用的是3.5.10版本
源码地址:https://github.com/mybatis/mybatis-3/releases
文档地址:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html

Mybatis相关全览

一. Mybatis源码详解
二. Mybatis二级缓存详解
三. Mybatis三大执行器介绍
四. Mybatis拦截器源码详解

一、简介

拦截器我相信大家已经非常熟悉了,Mybatis也有拦截器?Mybatis提供了一种插件(plugin)的功能,实际上就是拦截器,像我们平时使用的分页插件也是基于此来实现的

在Mybatis中拦截器可以针对4个点拦截处理,也可以说是4个接口:

  • Executor:拦截执行器的相关方法,可以对这些方法增强处理
  • ParameterHandler:拦截参数的处理,可以对传参做统一处理
  • ResultSetHandler:拦截结果集的处理,可以对返回的结果做统一处理
  • StatementHandler:拦截Sql构建的处理

执行与添加顺序

拦截器主要是拦截以上4个接口,对里面的方法进行增强处理,所以如果以方法来看拦截器的执行顺序是与这几个接口里面的方法在Mybatis执行流程里面的顺序有关,但如果以接口来看拦截器的执行顺序,大概流程是这样(不了解的可以去看看 Mybatis流程源码):

ExecutorParameterHandlerStatementHandlerResultSetHandler

拦截器生效入口

拦截器是什么时候介入到执行流程中来的呢?

以上述4个接口为例,所以有4个介入入口,均是在执行流程中初始化的时候介入的,如下图:

Configuration内部:

在这里插入图片描述

二、使用

使用拦截器其实很简单,我们只需要打上两个注解,实现一个接口,并在配置文件中配置一下就可以了

要打上的两个注解:

  • @Intercepts: 该注解等于是个标识,标识该类是个拦截器,需要配合@Signature来使用
  • @Signature: 该注解也是个标识,表示要拦截的接口以及接口内的哪个方法,有三个参数
    • type :代表我们要拦截的是哪个接口(4个里面选一个)
    • method :代表我们要拦截的是接口里面的哪一个方法(从接口里面去选一个)
    • args :要拦截的方法里面的参数类型,方法里面有几个传参这里就要写几个(因为方法存在重载,名称还无法确定唯一性)

要实现的接口:

Interceptor,内部有三个方法,一个必须要实现,两个随意

  • intercept():必须要实现的方法,这里就可以处理我们的逻辑
  • plugin():可选择实现,返回目标对象或者代理对象
  • setProperties():获取参数,可以从外部获取一些配置参数

下面准备了一个小例子,大家先感受一下

例子

/*** 这里我们拦截Executor里面的query和update方法* 一个@Signature 代表要拦截的一个方法*/
@Intercepts({/*** type     :代表我们要拦截的是哪个接口(4个里面选一个)* method   :代表我们要拦截的是接口里面的哪一个方法(从接口里面去选一个)* args     :要拦截的方法里面的参数类型,方法里面有几个传参这里就要写几个(因为方法存在重载,名称还无法确定唯一性)**/@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class})
})
public class DemoInterceptor1 implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {//被代理对象Object target = invocation.getTarget();//代理方法Method method = invocation.getMethod();//方法参数Object[] args = invocation.getArgs();// do something ...方法拦截前执行代码块System.out.println("方法拦截前执行 do something ...");// 本方法执行(这就是执行被拦截的源方法)Object result = invocation.proceed();// do something ...方法拦截后执行代码块System.out.println("方法拦截后执行 do something ...");return result;}/*** 通过该方法决定要返回的对象是目标对象还是对应的代理* 一般就两种情况(乱来小心报错):* 1. return target;  直接返回目标对象,相当于当前Interceptor没起作用,不会调用上面的intercept()方法* 2. return Plugin.wrap(target, this);  返回代理对象,会调用上面的intercept()方法*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 用于获取在Configuration初始化当前的Interceptor时候设置的一些参数** @param properties Properties参数*/@Overridepublic void setProperties(Properties properties) {}
}

配置文件中配置:

  

由于现在一般都结合springboot开发,这种配置文件的方式可能都被舍弃了,所以在springboot中我们只需要采用@Configuration+@Bean的方式把我们拦截器注入就可以了

三、原理

拦截器类似AOP都属于切面编程思想,底层原理都是动态代理

加载入口

我们以XML配置为例,在解析XML配置的时候就会解析插件(plugins)节点:

在这里插入图片描述

内部就会加载这些拦截器到configuration当中:

在这里插入图片描述

生成代理

还记得上面拦截器的生效入口吗?都执行了一次pluginAll方法吧

  1. 该方法内部就会遍历所有拦截器,执行拦截器里面的plugin方法
  2. 然后通过拦截器打上的那两个注解去匹配
  3. 有匹配的我就给你生成一个代理对象返回,没有就返回原本的对象

在这里插入图片描述

遍历拦截器

在这里插入图片描述

该方法就是我们实现接口后可选择实现的方法之一,有个默认实现的逻辑

在这里插入图片描述

匹配&生成代理

Plugin.wrap方法 就会就获取拦截器上的注解,然后与拦截器的目标对象去匹配,如果匹配上了说明在拦截的范围所以会生成一个代理对象,Plugin同时又实现了InvocationHandler接口,说明代理对象最后执行的时候,会执行Plugin.invoke方法,内部就会判断执行的方法是否是我需要拦截的方法,是则会执行拦截器的拦截方法,不是则执行原方法

在这里插入图片描述

原理咧就是这样,动态代理,对动态代理不了解的,可以先去了解一下

四、实践例子

这里根据上面demo搞了个例子,用处就是打印执行日志,执行的具体方法、执行的完整的SQL语句、执行时间

/*** 这里我们拦截Executor里面的query和update方法* 一个@Signature 代表要拦截的一个方法*/
@Intercepts({/*** type     :代表我们要拦截的是哪个接口(4个里面选一个)* method   :代表我们要拦截的是接口里面的哪一个方法(从接口里面去选一个)* args     :要拦截的方法里面的参数类型,方法里面有几个传参这里就要写几个(因为方法存在重载,名称还无法确定唯一性)**/@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class})
})
public class DemoInterceptor1 implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = null;if (invocation.getArgs().length > 1) {parameter = invocation.getArgs()[1];}String sqlId = mappedStatement.getId();BoundSql boundSql = mappedStatement.getBoundSql(parameter);Configuration configuration = mappedStatement.getConfiguration();long sqlStartTime = System.currentTimeMillis();Object re = invocation.proceed();long sqlEndTime = System.currentTimeMillis();// 获取SQL执行语句String s = sqlHandler(boundSql, configuration);// 打印mysql执行 日志  这里为了方便,不要学我System.out.println("-----------------------------------------------------------------");System.out.println("SQL的Mapper方法: "+sqlId);System.out.println("SQL: "+s);System.out.println("SQL执行时间:" + (sqlEndTime - sqlStartTime) + " ms");System.out.println("-----------------------------------------------------------------");return re;}/*** @Author czl* @Description   sql语句里面的?替换成真实的参数**/private String sqlHandler(BoundSql boundSql,Configuration configuration){// 获取mapper里面方法上的参数Object sqlParameter = boundSql.getParameterObject();TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();// sql原始语句(?还没有替换成我们具体的参数)String sql = boundSql.getSql().replaceAll("[\\s]+", " ");List parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (sqlParameter == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(sqlParameter.getClass())) {value = sqlParameter;} else {MetaObject metaObject = configuration.newMetaObject(sqlParameter);value = metaObject.getValue(propertyName);}// 上面都是搬的源码里面的  这里的value应该还需要处理,我这里就无脑转String了sql = sql.replaceFirst("\\?", value.toString());}}}return sql;}/*** 通过该方法决定要返回的对象是目标对象还是对应的代理* 一般就两种情况(乱来小心报错):* 1. return target;  直接返回目标对象,相当于当前Interceptor没起作用,不会调用上面的intercept()方法* 2. return Plugin.wrap(target, this);  返回代理对象,会调用上面的intercept()方法*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 用于获取在Configuration初始化当前的Interceptor时候设置的一些参数** @param properties Properties参数*/@Overridepublic void setProperties(Properties properties) {}

结果如下:

在这里插入图片描述

相关内容

热门资讯

适合年轻人小本创业好项目 适合...   :凉点美食店开一间冷点美食店,投资小、见效快,操作简单,一学就会。随着天气越来越热,吃凉点的人也...
五千元以内小本创业项目有哪些 ... 中医药销售具有自主的知识产权是中国中药最大的优势(这是其他医药子行业所不具备的),入世对于中国中药行...
适合穷人的小本创业项目 适合穷... 穷人不敢创业是为什么呢?因为害怕失去,害怕自己辛辛苦苦积攒下来的血汗钱赔个精光。如今生活中穷人的日子...
小本创业5个好项目 穷人告别打... 生活水平越来越高,这对于刚毕业的年轻人来说或许就产生了不小的压力,进入职场的时间不长,工资也没有太高...
昆明小本创业好项目 昆明小本创... 为什么穷人多不敢去创业蛋糕创业蛋糕店创业30岁女人创业做什么适合女性创业的大学生适合什么创业毕业生如...
创业板股票代码 创业板股票代码... 原标题:近5000万老股民注意!想参与注册制下创业板股票打新,赶紧做这件事!摘要【近5000万老股民...
在家小本创业办厂项目 在家小本... 准备在家办厂致富的你,知道现在在家办厂有什么项目?家庭小型投资办厂项目有哪些?现在在家投资,如果你可...
农民创业小本项目 农民创业小本... 我国是农业大国,农村人口众多,在农村乡镇,不少人觉得创业项目太少,无从选择。其实随着经济的发展,农村...
小本创业项目小本农村创业好项目... 我国是农业大国,农村人口众多,在农村乡镇,不少人觉得创业项目太少,无从选择。其实随着经济的发展,农村...
一人小本创业好项目 一人小本创... 一个人创业一直以来都是比较艰难的,但是也有人突破了所有的困难的成功者,虽然是小数,但也是值得大家去尝...