Mybatis插件体系
一、概述
到此为止,Mybatis的四大组件我们都已经介绍过了,下面通过时序图把各个组件的作用串联一下:
各组件核心作用:
Executor
:MyBatis 的执行核心,负责管理事务、缓存,调度StatementHandler
执行 SQL。StatementHandler
:负责与 JDBC 交互,创建Statement
、协调ParameterHandler
和ResultSetHandler
设置SQL参数、执行 SQL、处理返回值。ParameterHandler
:负责将 Java 参数转换为 JDBC 参数,设置到 Statement 的占位符中。ResultSetHandler
:负责将 JDBC 的ResultSet
转换为Java对象(如 POJO、List 等)。
二、MyBatis 插件体系及使用示例
1. 插件体系核心概念
Mybatis基于插件,提供了强大的扩展能力,允许在四大组件的方法执行前后插入自定义逻辑(如日志记录、性能监控、数据加密等)。实现Mybatis的插件,需要:
- 通过
@Intercepts
指定拦截四大组件中的哪个组件,通过@Signature
注解指定拦截的方法。注意,Mybatis通过JDK的动态代理实现,只能拦截四大组件接口中声明的方法 (如Executor
接口的query
方法),不能拦截实现类的私有方法或非接口方法。 - 实现
Interceptor
接口,拦截组件方法,可以在方法前后实现自定义逻辑。 - 在xml配置文件中,配置插件。
2. 插件使用示例:SQL 执行日志插件
下面我们实现一个简单的打印 SQL 语句、参数和执行时间的例子。
(1)定义插件类
我们先看下Interceptor
接口的定义:
java
public interface Interceptor {
// 核心方法:实现拦截逻辑,通过Invocation参数控制原方法执行
Object intercept(Invocation invocation) throws Throwable;
// 生成代理对象:默认调用Plugin.wrap
default Object plugin(Object target){
Plugin.wrap(target, this);
}
// 设置插件属性:读取配置文件中插件的参数
default void setProperties(Properties properties){}
}
核心是要实现intercept
方法,如果需要自定义配置,可以实现setProperties
方法。
java
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
// 声明拦截Executor的query和update方法
@Intercepts({
@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 SqlLogPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取拦截的方法和参数
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
String sql = ms.getBoundSql(parameter).getSql(); // 获取SQL语句
// 2. 执行前:打印SQL和参数
long startTime = System.currentTimeMillis();
System.out.printf("执行SQL:%s,参数:%s%n", sql, parameter);
// 3. 执行原方法(继续流程)
Object result = invocation.proceed();
// 4. 执行后:打印耗时
long endTime = System.currentTimeMillis();
System.out.printf("SQL执行完成,耗时:%dms%n", endTime - startTime);
return result;
}
// 读取插件配置的属性(如在mybatis-config.xml中配置)
@Override
public void setProperties(Properties properties) {
// 可读取配置的属性,如日志级别
String logLevel = properties.getProperty("logLevel", "INFO");
System.out.println("插件日志级别:" + logLevel);
}
}
(2)配置插件
在mybatis-config.xml
中注册插件:
xml
<configuration>
<plugins>
<plugin interceptor="com.example.SqlLogPlugin">
<!-- 可选:配置插件属性 -->
<property name="logLevel" value="DEBUG"/>
</plugin>
</plugins>
</configuration>
注:插件执行顺序由配置顺序决定,形成责任链。
(3)效果
执行 SQL 时,控制台会输出:
sql
执行SQL:SELECT * FROM user WHERE id = ?,参数:1
SQL执行完成,耗时:12ms
三、MyBatis 插件实现原理
MyBatis插件的核心是JDK动态代理 ,通过Plugin
类实现拦截逻辑。
1. 核心类与接口
Interceptor
:插件接口,自定义插件至少需要实现其intercept
方法;如果需要设置属性,可以实现setProperties
方法;一般不需要重新实现plugin
(新版本有默认实现)。Plugin
实现InvocationHandler
接口,是代理对象的实际处理器,负责调用拦截器和原方法。Invocation
:封装拦截方法的调用信息(目标对象、方法、参数),并提供proceed()
方法继续执行原逻辑。
2. 插件初始化流程
当 MyBatis 启动时,会解析mybatis-config.xml
中的<plugins>
标签,将插件实例存入Configuration
的interceptors
列表中,这里不不去看源码了。
3. 代理对象创建(Plugin.wrap()
)
当四大组件(如 Executor)初始化时,MyBatis 会通过InterceptorChain.pluginAll()
为其创建代理对象:
java
public Object pluginAll(Object target) {
// 遍历所有配置的插件,对目标对象(四大组件)进行代理
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 调用插件的plugin方法
}
return target;
}
自定义插件的实现,默认是调用Plugin类的wrap方法实现:
java
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 获取插件要拦截的方法映射(type -> 方法集合)
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 2. 确定需要代理的接口,也就是注解生命的接口与目标对象的接口的交集
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 3. 创建JDK动态代理(Plugin为InvocationHandler)
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)
);
}
return target; // 无需代理则返回原对象
}
4. 拦截逻辑执行(Plugin.invoke()
)
Plugin
是动态代理接口InvocationHandler
的实现类,当代理对象的方法被调用时,invoke
方法会触发拦截逻辑:
java
// Plugin类的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 检查当前方法是否在插件的拦截列表中
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 2. 若需要拦截,调用插件的intercept方法(传入Invocation)
return interceptor.intercept(new Invocation(target, method, args));
}
// 3. 无需拦截,直接执行原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
// Invocation类的proceed方法(继续执行原逻辑)
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args); // 调用被代理对象的原方法
}
Invocation
处理方法和参数,还提供了继续执行的proceed
方法:
java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args); // 调用被代理对象的原方法
}
关键说明:
Invocation.proceed()
是流程继续的核心:若target
是原始对象,则执行原方法;若target
是上一层代理,则触发该代理的invoke
方法,形成链式调用;- 插件的
intercept
方法中,必须调用invocation.proceed()
,否则会中断原方法执行(除非业务需要主动阻断)。
5. 责任链执行
若配置多个插件,MyBatis 会为组件创建多层代理 (每层代理对应一个插件)。调用方法时,代理会按插件配置顺序依次执行intercept
方法,最终执行原方法。现在假设我们有两个插件A/B,那么执行流程为:
说明:
- 插件配置顺序:PluginA 先于 PluginB 配置(代理链:插件 B 代理 → 插件 A 代理 → 原始对象)
- 执行顺序:
- 外层插件 B 先执行前置逻辑
- 调用 proceed 后进入内层插件 A
- 插件 A 执行前置逻辑并调用 proceed 进入原始对象
- 原始对象返回后,插件 A 先执行后置逻辑
- 最终插件 B 执行后置逻辑并返回结果
四、总结
MyBatis 插件机制基于 JDK 动态代理和责任链模式,通过Interceptor
接口定义扩展点,Plugin
类实现代理逻辑,InterceptorChain
管理插件链,最终实现对四大组件的灵活拦截。其核心优势在于:
- 低侵入性:无需修改框架源码,通过配置即可扩展功能;
- 灵活性:支持单个插件拦截多个组件,或多个插件协同工作;
- 精确性 :通过
@Signature
精确控制拦截的方法,避免不必要的性能损耗。
一些知名的Mybatis的扩展,都是通过插件实现的,比如
- 分页:pageHelper
- Mybatis增强:mybats-plus
在实际使用中,需注意插件的执行顺序和性能影响,避免过度拦截导致的效率问题。理解插件的实现原理,不仅能更好地使用现有插件,还能根据业务需求开发自定义插件,充分发挥 MyBatis 的扩展能力。