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流程源码):
Executor→ParameterHandler→StatementHandler→ResultSetHandler
拦截器生效入口
拦截器是什么时候介入到执行流程中来的呢?
以上述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 {
@Override
public 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()方法
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 用于获取在Configuration初始化当前的Interceptor时候设置的一些参数
*
* @param properties Properties参数
*/
@Override
public void setProperties(Properties properties) {
}
}
配置文件中配置:
<plugins>
<plugin interceptor="org.apache.ibatis.interceptor.DemoInterceptor"/>
</plugins>
由于现在一般都结合springboot开发,这种配置文件的方式可能都被舍弃了,所以在springboot中我们只需要采用@Configuration+@Bean的方式把我们拦截器注入就可以了
三、原理
拦截器类似AOP都属于切面编程思想,底层原理都是动态代理
加载入口
我们以XML配置为例,在解析XML配置的时候就会解析插件(plugins)节点:
内部就会加载这些拦截器到configuration当中:
生成代理
还记得上面拦截器的生效入口吗?都执行了一次pluginAll方法吧
- 该方法内部就会遍历所有拦截器,执行拦截器里面的plugin方法
- 然后通过拦截器打上的那两个注解去匹配
- 有匹配的我就给你生成一个代理对象返回,没有就返回原本的对象
遍历拦截器
该方法就是我们实现接口后可选择实现的方法之一,有个默认实现的逻辑
匹配&生成代理
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 {
@Override
public 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<ParameterMapping> 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()方法
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 用于获取在Configuration初始化当前的Interceptor时候设置的一些参数
*
* @param properties Properties参数
*/
@Override
public void setProperties(Properties properties) {
}
结果如下: