Mybatis的插件原理
一句话总结
MyBatis 插件本质是基于拦截器的动态代理:开发者实现 Interceptor 接口并声明要拦截的目标(Executor、StatementHandler 等),MyBatis 在创建这些核心组件时通过责任链模式套上一层层代理,方法执行时依次经过所有插件增强逻辑,最后再调用原始方法。常用于 SQL 改写、分页、日志、审计、加解密等横切功能。
详细解析
MyBatis 的插件机制是其核心扩展点之一,通过拦截 + 动态代理 + 责任链把横切逻辑从业务代码里抽离出来,非常适合做 AOP 风格的增强。
一、插件运行原理的核心架构
-
拦截器接口
Interceptor- 插件必须实现
org.apache.ibatis.plugin.Interceptor接口,包含三个核心方法:
javapublic interface Interceptor { Object intercept(Invocation invocation) throws Throwable; // 真正的拦截逻辑 Object plugin(Object target); // 为目标对象生成代理 void setProperties(Properties properties); // 读取配置属性 }- 职责 :
intercept:你要插入的自定义逻辑(打印 SQL、改写 SQL、统计耗时等)写在这里;plugin:通常直接调用Plugin.wrap(target, this),返回代理;setProperties:读取mybatis-config.xml中<property>传入的参数(如日志级别、开关等)。
- 插件必须实现
-
动态代理机制
- MyBatis 使用 JDK 动态代理 为四大对象生成代理:
ExecutorStatementHandlerParameterHandlerResultSetHandler
- 这里的意思可以理解成:
- 原来你直接用的是"真实对象"(比如某个
Executor实现类),现在 MyBatis 在外面再套一层"代理对象"; - 以后你的代码拿到的不是原始
Executor,而是它的"代理壳"; - 调用过程(
invoke→ 多个intercept)可以想象成这样:- 你调用
executor.update(...); - 实际上先调用到的是"代理对象"的
invoke(...)方法; - 这个
invoke里面,MyBatis 会把方法、参数等封装成一个Invocation对象,然后按顺序把这个Invocation传给每一个插件的intercept(Invocation); - 每个插件的
intercept里可以先做自己的逻辑(改 SQL、打日志、统计耗时、做分页等),然后调用invocation.proceed()把调用继续传下去; - 当所有插件都执行完并都调用了
proceed()之后,才真正执行到底层原始Executor的update方法。
- 你调用
- 方法不会直接打到真实对象,而是先经过一层代理,在代理的
invoke方法里,MyBatis 挨个调用所有插件的intercept,组成一条调用链。
- 原来你直接用的是"真实对象"(比如某个
- 一句话:动态代理 = 在目标对象外面套一层壳,所有方法调用先经过这层壳,让插件有机会"插一脚"。
- MyBatis 使用 JDK 动态代理 为四大对象生成代理:
-
插件链(InterceptorChain,责任链模式)
- MyBatis 会根据
<plugins>配置顺序,将多个插件依次包装 到目标对象外层,形成一条调用链:- 可以想象成给目标对象一层一层"套娃":最先配置的插件在最外面,后配置的在里面,最中心才是真实对象;
- 调用方法时,先进入最外层插件的
intercept,它在合适的时候调用invocation.proceed()把调用"传下去"给下一层插件; - 下一层插件同样先执行自己的
intercept,再通过proceed()继续往里传; - 一直传到最里面的真实对象,才真正执行原始方法,然后一层一层返回结果。
- 那句"最外层插件先执行 → 调
invocation.proceed()→ 交给下一个插件 → 直到最内层原始对象"就是说的这个从外到内、逐层传递调用 的过程;这就是经典的责任链模式 ,可以让多个插件按顺序协作增强同一个方法调用。
- MyBatis 会根据
二、插件执行流程(从配置到生效)
- 在
mybatis-config.xml中注册插件
xml
<plugins>
<plugin interceptor="com.example.mybatis.LoggingInterceptor">
<property name="logLevel" value="DEBUG"/>
</plugin>
</plugins>
- 通过注解声明拦截点
java
@Intercepts({
@Signature(
type = Executor.class, // 要拦截的接口
method = "update", // 要拦截的方法
args = {MappedStatement.class, Object.class} // 方法参数列表
)
})
public class LoggingInterceptor implements Interceptor {
// ...
}
-
MyBatis 初始化时包装目标对象
- 启动 / 创建
SqlSessionFactory过程中,MyBatis 会:- 读取
<plugins>配置,创建各个Interceptor实例; - 针对上述四大核心接口,依次调用每个插件的
plugin(target)方法; - 如果当前插件声明要拦截这个
target,就用Plugin.wrap返回代理,否则直接返回原对象。
- 读取
- 启动 / 创建
-
执行 SQL 时的调用顺序
- 业务代码调用
SqlSession.update()/select()→ - 调用链进入最外层插件代理对象的
invoke→ - 触发该插件的
intercept(Invocation)→ - 插件内部可以:
- 读取 / 修改参数:
invocation.getArgs(); - 在调用前后做日志、计时、监控;
- 决定是否、何时调用
invocation.proceed()。
- 读取 / 修改参数:
invocation.proceed()会把调用交给下一个插件或最终原始对象。
- 业务代码调用
三、插件开发示例
- SQL 日志与耗时统计插件
java
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}
)
})
public class SqlLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
long start = System.currentTimeMillis();
System.out.println("[MyBatis] SQL: " + sql);
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
System.out.println("[MyBatis] Cost: " + cost + " ms");
return result;
}
@Override
public Object plugin(Object target) {
// 统一交给 MyBatis 提供的 Plugin 工具来创建代理
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以从 properties 中读取自定义配置,比如日志级别
}
}
- 简化版分页插件示意
java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SimplePaginationInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql();
// 示例:强行拼一个 LIMIT 子句,真实场景会从 ThreadLocal / 参数中取 pageNum、pageSize
String pageSql = originalSql + " LIMIT 0, 10";
// 通过反射修改 BoundSql 中的 sql 字段
Field field = BoundSql.class.getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, pageSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
上面只是原理 Demo,真正生产级分页通常使用 PageHelper、MyBatis-Plus 等成熟插件。
四、常见插件应用场景
| 场景 | 拦截接口 / 方法 | 典型插件 / 功能 |
|---|---|---|
| SQL 日志记录 | Executor.update / Executor.query |
自定义日志插件、p6spy、Logback 适配 |
| 分页查询 | StatementHandler.prepare |
PageHelper、MyBatis-Plus 分页插件 |
| 性能监控 | 包裹所有 SQL 执行接口 | 统计耗时、慢查询告警 |
| 数据加密/解密 | ParameterHandler.setParameters / ResultSetHandler |
字段加解密、脱敏 |
| 审计与埋点 | Executor.update / commit / rollback |
记录操作人、时间、数据快照 |
五、面试回答模板(可直接复述)
- 问:MyBatis 插件原理是什么?能拦截哪些地方?
- 答:"MyBatis 插件基于拦截器 + JDK 动态代理 + 责任链模式。我们实现
Interceptor接口并用@Intercepts/@Signature声明要拦截的接口和方法,MyBatis 在创建Executor、StatementHandler、ParameterHandler、ResultSetHandler时按<plugins>配置顺序一层层用Plugin.wrap包装成代理对象。调用这些方法时会依次进入各个插件的intercept,在里面可以做 SQL 改写、日志、分页、加解密等增强逻辑,最后通过invocation.proceed()调回原始方法。"
- 答:"MyBatis 插件基于拦截器 + JDK 动态代理 + 责任链模式。我们实现