插件原理与开发
插件原理与开发
在 Mybatis总体执行流程 一文中简单的介绍了插件的初始化过程,本文将从源码的角度介绍一下mybatis的插件原理与简单开发实战。
插件原理
插件的注册和管理是通过InterceptorChain进行的,在创建Executor、StatementHandler、ParameterHandler、ResultSetHandler对象时,会执行InterceptorChain的pluginAll方法
public Object pluginAll(Object target) {
// 遍历所有的插件
for (Interceptor interceptor : interceptors) {
// 执行插件的plugin方法,返回代理对象
target = interceptor.plugin(target);
}
return target;
}
拦截的原理,正是此时返回的代理对象,当调用目标方法时,执行的就是拦截器的intercept方法,从而实现拦截功能。
// 执行插件的plugin方法,返回代理对象
target = interceptor.plugin(target);
来到Interceptor接口的plugin方法:
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
这是一个默认方法,一般不会重写它的逻辑。看其实现Plugin#wrap:
public static Object wrap(Object target, Interceptor interceptor) {
// 拿到拦截器的@Intercepts注解信息:key是要拦截的接口,value是要拦截的接口方法集合
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 这里的target,就是拦截的对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler对象)
Class<?> type = target.getClass();
// 返回包含在signatureMap中的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 存在被拦截的接口,返回一个代理对象
if (interfaces.length > 0) {
// 利用jdk动态代理生成代理对象:关注Plugin(实现了InvocationHandler接口)的invoke方法
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 接口没有被拦截,返回原始对象
return target;
}
可以看到,如果接口被拦截了,就会利用JDK动态代理生成代理对象,由于Plugin实现了InvocationHandler接口,所以其invoke方法会被执行:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 拿到被拦截的接口方法集合
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 判断当前执行的方法是否包含在其中,包含就是被拦截的方法
if (methods != null && methods.contains(method)) {
// 执行自定义拦截器的intercept方法,并将目标对象、方法、参数传入
return interceptor.intercept(new Invocation(target, method, args));
}
// 否则直接执行原始方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
插件开发
自定义插件需要:
-
实现Interceptor接口,重写intercept方法
-
使用@Intercepts和@Signature注解表明需要拦截哪些类的哪些方法
-
在配置文件中,添加插件配置
mybatis官网中,对此也有所描述:mybatis – MyBatis 3 | Configuration
根据官网描述,mybatis插件可以拦截的方法如下:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
以下是我写的一个记录SQL及其耗时的拦截器,加深对拦截器的理解:
/**
* @Author: qiuxinfa
* @CreateTime: 2023-12-07 22:15
* @Description: 自定义拦截器:打印SQL、统计SQL执行时间
*/
@Intercepts({
@Signature(type = StatementHandler.class,method = "batch",args = {Statement.class}),
@Signature(type = StatementHandler.class,method = "update",args = {Statement.class}),
@Signature(type = StatementHandler.class,method = "query",args = {Statement.class, ResultHandler.class}),
})
public class SqlLogPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行的SQL
String sql;
Statement statement=(Statement) invocation.getArgs()[0];
if(Proxy.isProxyClass(statement.getClass())){
MetaObject metaObject= SystemMetaObject.forObject(statement);
Object h = metaObject.getValue("h");
if(h instanceof StatementLogger){
RoutingStatementHandler rsh=(RoutingStatementHandler) invocation.getTarget();
sql = rsh.getBoundSql().getSql();
}else {
PreparedStatementLogger psl=(PreparedStatementLogger) h;
sql = psl.getPreparedStatement().toString();
}
}else{
sql = statement.toString();
}
// 记录开始时间
long start = System.currentTimeMillis();
// 执行目标方法
Object result = invocation.proceed();
// 记录结束时间
long end = System.currentTimeMillis();
System.err.println("执行SQL ===> ");
System.err.println(sql);
System.err.println("统计SQL耗时 = " + (end - start) + "毫秒");
System.err.println("返回结果 =======> " + result);
return result;
}
}
配置文件添加插件:
<!-- 配置插件 -->
<plugins>
<plugin interceptor="com.qxf.plugin.SqlLogPlugin"></plugin>
</plugins>
配置之后,会打印执行的SQL语句及其耗时。