plugins
导学
本节学习目标:
- 理解
plugins是 MyBatis 提供的"横切干预"机制,可在 SQL 执行全链路中插入自定义逻辑 - 掌握四大可拦截对象:
Executor、StatementHandler、ParameterHandler、ResultSetHandler - 能够使用
@Intercepts和@Signature注解编写一个完整的插件 - 了解插件的注册方式与拦截链执行顺序
定义
在实际项目中,我们经常需要在不修改原有 Mapper 和业务代码的前提下,为 SQL 执行增加通用功能:
- 打印执行的 SQL 语句及耗时,方便调试
- 对慢 SQL 进行监控和告警
- 实现物理分页逻辑
- 对查询结果进行数据脱敏或权限过滤
MyBatis 的 plugins 机制通过动态代理拦截四大核心接口的方法调用,让开发者可以在 SQL 执行前后插入自定义逻辑,是实现上述需求的官方标准方式。
适用位置与核心属性
plugins 位于 mybatis-config.xml 中 objectFactory 之后、environments 之前。
<plugins>
<plugin interceptor="com.feixiang.plugin.SqlExecutionTimePlugin">
<property name="slowThreshold" value="1000"/>
</plugin>
</plugins>
| 属性 | 是否必填 | 说明 |
|---|---|---|
interceptor | 必填 | 插件类的全限定名,必须实现 Interceptor 接口 |
子标签 property 用于向插件实例注入配置属性。
核心注解
| 注解 | 作用 |
|---|---|
@Intercepts | 声明这是一个拦截器,内部包含一个或多个 @Signature |
@Signature | 声明具体拦截哪个接口的哪个方法,包含 type(接口类)、method(方法名)、args(参数类型数组) |
核心原理
插件拦截链流程图
动态代理机制:MyBatis 使用 JDK 动态代理为四大对象创建代理实例。每个插件按注册顺序层层包裹目标对象,形成责任链。调用时从外层插件依次向内执行,返回时从内层依次向外执行。
可拦截的四大对象及常用方法:
| 接口 | 常用拦截方法 | 典型应用场景 |
|---|---|---|
Executor | query, update, commit, rollback | 分页、缓存、事务监控 |
StatementHandler | prepare, parameterize, batch, update, query | SQL 改写、打印 |
ParameterHandler | setParameters | 参数加密、参数校验 |
ResultSetHandler | handleResultSets, handleOutputParameters | 结果脱敏、数据过滤 |
完整示例
场景说明
乐途公司员工管理系统的开发团队需要监控所有 SQL 的执行耗时,当某条 SQL 执行超过 1000ms 时,在日志中输出慢查询告警。要求不修改任何 Mapper XML 和业务代码。
操作前的状态
未配置插件时,MyBatis 仅输出 DEBUG 级别的 SQL 语句,无法统计执行耗时,也无法识别慢查询。
完整配置代码
步骤一:实现 SQL 执行耗时监控插件
package com.feixiang.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SqlExecutionTimePlugin implements Interceptor {
private long slowThreshold = 1000; // 慢查询阈值,单位毫秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取目标 StatementHandler
StatementHandler target = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(target);
// 获取即将执行的 SQL(通过 BoundSql)
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
sql = sql.replaceAll("[\s]+", " "); // 压缩多余空白
long start = System.currentTimeMillis();
try {
// 执行目标方法
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > slowThreshold) {
System.out.println("【慢查询告警】SQL: " + sql + " | 耗时: " + cost + "ms");
} else {
System.out.println("【SQL监控】SQL: " + sql + " | 耗时: " + cost + "ms");
}
}
}
@Override
public Object plugin(Object target) {
// 使用 MyBatis 提供的 Plugin.wrap 创建代理
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
String threshold = properties.getProperty("slowThreshold");
if (threshold != null) {
this.slowThreshold = Long.parseLong(threshold);
}
System.out.println("【SqlExecutionTimePlugin】初始化完成,慢查询阈值: " + slowThreshold + "ms");
}
}
步骤二:在 mybatis-config.xml 中注册插件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.feixiang.entity"/>
</typeAliases>
<!-- 插件配置 -->
<plugins>
<plugin interceptor="com.feixiang.plugin.SqlExecutionTimePlugin">
<property name="slowThreshold" value="1000"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
步骤三:Java 测试代码
public class PluginDemo {
public static void main(String[] args) throws Exception {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
try (SqlSession session = factory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
// 执行查询
Employee emp = mapper.selectById(1);
System.out.println("查询结果: " + emp.getEmployeeName());
}
}
}
实际效果/结果
控制台输出:
【SqlExecutionTimePlugin】初始化完成,慢查询阈值: 1000ms
【SQL监控】SQL: SELECT employee_id, employee_name, department_code, create_time FROM employee WHERE employee_id = ? | 耗时: 15ms
查询结果: 王五
若模拟慢查询(如在数据库端添加 SLEEP(2)):
【慢查询告警】SQL: SELECT employee_id, employee_name, department_code, create_time FROM employee WHERE employee_id = ? | 耗时: 2015ms
分析
@Signature必须精确匹配方法的参数类型数组,包括顺序。StatementHandler.prepare(Connection, Integer)的参数是Connection.class和Integer.class,不能写错Plugin.wrap(target, this)是 MyBatis 提供的标准代理创建方式,它会检查目标对象是否属于@Signature中声明的接口类型,只有匹配时才创建代理invocation.proceed()是调用链继续向下执行的关键,若忘记调用则目标方法不会执行- 多个插件按 XML 中注册顺序层层包裹,先注册的插件在外层,后注册的在内层
易错场景/常见误区
| 误区 | 错误表现 | 正解 |
|---|---|---|
@Signature 的 args 写错参数类型 | 插件完全不生效,目标方法直接执行 | 必须与被拦截方法的参数类型、顺序完全一致。注意包装类与基本类型的区别(如 Integer.class 而非 int.class) |
忘记调用 invocation.proceed() | SQL 不执行,返回 null 或空结果 | intercept 方法中必须通过 invocation.proceed() 将调用传递给下一个插件或目标对象 |
使用 Plugin.wrap 但目标对象未实现 @Signature 中的接口 | 代理未创建,插件不生效 | Plugin.wrap 内部会检查接口匹配性,确保 type 属性与目标对象类型一致 |
| 多个插件顺序不当导致逻辑混乱 | 分页插件在慢查询插件之后,导致耗时统计不包含分页改写时间 | 理解代理链顺序:先注册在外层。根据业务需求调整 XML 中 <plugin> 的排列顺序 |
试图拦截 SqlSession 的方法 | @Signature(type = SqlSession.class) 不生效 | 插件只能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四个接口 |
在 plugin() 方法中直接返回 this | 类型转换异常,启动失败 | 必须返回 Plugin.wrap(target, this) 创建的代理对象,而非拦截器自身 |
面试考点
Q1:MyBatis 插件可以拦截哪些对象?分别适用于什么场景?
A:可以拦截四大核心对象:
Executor:拦截增删改查和事务操作,适用于分页、缓存、全局事务监控StatementHandler:拦截 SQL 的预编译和执行,适用于 SQL 打印、改写、耗时监控ParameterHandler:拦截参数设置,适用于参数加密、脱敏、校验ResultSetHandler:拦截结果集处理,适用于结果脱敏、数据权限过滤、结果转换
Q2:多个插件同时注册时,它们的执行顺序是怎样的?
A:MyBatis 按照 XML 中
<plugin>的注册顺序,使用责任链模式层层包裹目标对象。先注册的插件在外层,后注册的在内层。方法调用时从外层向内层传递,返回时从内层向外层回溯。因此外层插件的 "前置逻辑" 最先执行,"后置逻辑" 最后执行。
Q3:为什么 MyBatis 插件基于 JDK 动态代理而不是 CGLIB?
A:因为被拦截的四大对象(
Executor、StatementHandler等)都是接口实现类,JDK 动态代理足以满足需求。使用 JDK 代理更轻量,且避免了 CGLIB 可能带来的类加载和字节码操作问题。MyBatis 的Plugin.wrap()内部正是使用Proxy.newProxyInstance()实现。
Q4:如何确保插件只拦截特定的方法而不影响其他方法?
A:通过
@Signature精确声明type(接口类)、method(方法名)和args(参数类型数组)。Plugin.wrap()在创建代理时会检查当前目标对象是否实现了@Signature中声明的接口,以及当前方法是否匹配签名,只有完全匹配才会进入intercept方法。
小结
plugins 是 MyBatis 扩展能力的"瑞士军刀",它通过责任链动态代理机制,在不侵入业务代码的前提下实现了 SQL 监控、分页、脱敏等横切需求。编写插件的关键在于:精确声明 @Signature、正确调用 invocation.proceed()、使用 Plugin.wrap() 创建代理。理解拦截链的顺序和四大对象的分工,是写出高质量插件的基础。
下一章引子
插件让我们能够干预 SQL 执行的内部链路,但 SQL 最终要落到具体的数据库上执行。不同的环境(开发、测试、生产)需要连接不同的数据库,甚至同一应用需要支持多套数据源。MyBatis 的 environments 元素提供了多环境配置与切换的能力,接下来我们将学习如何配置和管理数据库环境。