一、先搞懂:MyBatis 拦截器到底能干嘛?
MyBatis 的拦截器本质是JDK 动态代理,能在 SQL 执行的关键节点 "插队" 执行我们的逻辑。它能拦截 4 个核心组件,覆盖 SQL 从生成到结果返回的全流程:
简单说:想监控 SQL 执行时间,拦Executor;想改 SQL(比如加租户条件),拦StatementHandler;想脱敏返回结果,拦ResultSetHandler。
拦截器必须实现Interceptor接口,核心就 3 个方法:
java
public interface Interceptor {
// 核心:拦截逻辑写这里
Object intercept(Invocation invocation) throws Throwable;
// 生成代理对象(直接用Plugin.wrap()就行)
Object plugin(Object target);
// 读取配置参数(比如从配置文件拿慢查询阈值)
void setProperties(Properties properties);
}
还要用@Intercepts和@Signature注解告诉 MyBatis 要拦谁、拦哪个方法:
java
// 示例:拦截StatementHandler的prepare方法(SQL预编译阶段)
@Intercepts({
@Signature(
type = StatementHandler.class, // 拦截哪个组件
method = "prepare", // 拦截组件的哪个方法
args = {Connection.class, Integer.class} // 方法参数类型(必须严格匹配)
)
})
public class MySqlInterceptor implements Interceptor {
// 实现接口方法...
}
踩坑提醒:args参数必须和方法实际参数类型完全一致!比如prepare方法有两个重载,记错参数类型就会拦截失败。
二、实战一:慢查询监控拦截器(一行代码定位超时 SQL)
需求:自动记录所有 SQL 的执行时间,超过 500ms 就报警,包含完整 SQL 和参数。实现步骤:
- 写拦截器逻辑(拦 Executor 的 query 和 update 方法)
java
@Slf4j
@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 SlowSqlInterceptor implements Interceptor {
// 慢查询阈值(默认500ms,可配置)
private long slowThreshold=500;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 记录开始时间
long startTime = System.currentTimeMillis();
try {
// 2. 执行原SQL(继续流程)
return invocation.proceed();
} finally {
// 3. 计算耗时
long costTime = System.currentTimeMillis() - startTime;
// 4. 获取SQL和参数
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
String sql = mappedStatement.getBoundSql(parameter).getSql(); // 带?的SQL
// 5. 慢查询报警
if (costTime > slowThreshold) {
log.warn("[慢查询警告] 耗时: {}ms, SQL: {}, 参数: {}", costTime, sql, parameter);
} else {
log.info("[SQL监控] 耗时: {}ms, SQL: {}", costTime, sql);
}
}
}
@Override
public Object plugin(Object target) {
// 生成代理对象(MyBatis工具方法,不用自己写)
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 从配置文件读阈值(比如application.yml里配)
String threshold = properties.getProperty("slowThreshold");
if (threshold != null) {
this.slowThreshold = Long.parseLong(threshold);
}
}
}
- 注册拦截器到 SpringBoot
java
@Configuration
@MapperScan("com.example.mapper")// 扫描Mapper接口
public class MyBatisConfig {
// 注册慢查询拦截器
@Bean
public SlowSqlInterceptor slowSqlInterceptor() {
SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
// 配置阈值(也可以在application.yml里配)
Properties props = new Properties();
props.setProperty("slowThreshold", "500"); // 500ms
interceptor.setProperties(props);
return interceptor;
}
// 把拦截器加到SqlSessionFactory
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, SlowSqlInterceptor slowSqlInterceptor) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置Mapper.xml路径(如果需要)
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
// 关键:添加拦截器
sessionFactory.setPlugins(slowSqlInterceptor);
return sessionFactory.getObject();
}
}
- 测试效果
调用任意查询接口,控制台会自动打印:
java
[SQL监控] 耗时: 32ms, SQL: SELECT id,username,phone FROM user WHERE id = ?
如果 SQL 执行超过 500ms(比如查大数据量表):
java
[慢查询警告] 耗时: 1200ms, SQL: SELECT * FROM order WHERE user_id = ?, 参数: 10086
关键优势:不用改任何 Service 或 Mapper 代码,所有 SQL 自动被监控。
三、实战二:数据脱敏拦截器(手机号、身份证自动打码)
需求:查询用户信息时,自动把手机号(13812345678→1385678)、身份证号(110...1234→************34)脱敏。
实现步骤:
- 自定义脱敏注解(标记需要脱敏的字段)
java
// 作用在字段上的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
SensitiveType type(); // 脱敏类型(手机号/身份证)
}
// 脱敏类型枚举
public enum SensitiveType {
PHONE, ID_CARD
}
- 实体类加注解(告诉哪些字段要脱敏)
java
@Data
public class User {
private Long id;
private String username;
@Sensitive(type = SensitiveType.PHONE) // 手机号脱敏
private String phone;
@Sensitive(type = SensitiveType.ID_CARD) // 身份证脱敏
private String idCard;
}
- 脱敏工具类(实现具体打码逻辑)
java
public class SensitiveUtils {
// 手机号脱敏:保留前3后4
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone; // 非手机号不处理
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
// 身份证脱敏:保留最后2位
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 18) {
return idCard; // 非身份证不处理
}
return idCard.replaceAll("\\d{16}(\\d{2})", "****************$1");
}
}
- 写结果集拦截器(拦 ResultSetHandler 处理返回结果)
java
@Slf4j
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class} // 拦截结果处理方法
)
})
public class SensitiveInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 先执行原方法,拿到查询结果
Object result = invocation.proceed();
// 2. 如果是List,遍历处理每个元素
if (result instanceof List<?>) {
List<?> resultList = (List<?>) result;
for (Object obj : resultList) {
desensitize(obj); // 脱敏处理
}
}
return result;
}
// 反射处理对象中的敏感字段
private void desensitize(Object obj) throws IllegalAccessException {
if (obj == null) return;
Class<?> clazz = obj.getClass();
// 遍历所有字段
for (Field field : clazz.getDeclaredFields()) {
// 3. 检查字段是否有@Sensitive注解
if (field.isAnnotationPresent(Sensitive.class)) {
Sensitive annotation = field.getAnnotation(Sensitive.class);
field.setAccessible(true); // 允许访问私有字段
Object value = field.get(obj); // 获取字段值
// 4. 根据类型脱敏
if (value instanceof String) {
String strValue = (String) value;
switch (annotation.type()) {
case PHONE:
field.set(obj, SensitiveUtils.maskPhone(strValue));
break;
case ID_CARD:
field.set(obj, SensitiveUtils.maskIdCard(strValue));
break;
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
- 注册多个拦截器(注意顺序!)
java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// 慢查询拦截器(先注册)
@Bean
public SlowSqlInterceptor slowSqlInterceptor() { ... }
// 脱敏拦截器(后注册)
@Bean
public SensitiveInterceptor sensitiveInterceptor() {
return new SensitiveInterceptor();
}
// 关键:多个拦截器的顺序就是执行顺序
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource,
SlowSqlInterceptor slowSqlInterceptor,
SensitiveInterceptor sensitiveInterceptor) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 按执行顺序添加:先监控SQL,再处理结果
sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);
return sessionFactory.getObject();
}
}
- 测试效果
查询用户后返回的对象自动脱敏:
java
User user = userService.getById(1L);
System.out.println(user);
// 输出:User(id=1, username=张三, phone=138****5678, idCard=****************34)
三、避坑指南:这 3 个错误 90% 的人都会犯
- 拦截器顺序搞反
多个拦截器时,注册顺序就是执行顺序。比如先注册脱敏拦截器,再注册慢查询拦截器,会导致慢查询日志里的参数已经被脱敏,排查问题时看不到原始值。
正确顺序:按 "SQL 执行前→执行中→执行后" 排序,比如:权限拦截器(改 SQL)→慢查询拦截器(监控)→脱敏拦截器(处理结果)。
- @Signature 的 args 参数写错
拦截不到方法大概率是因为args参数类型和目标方法不一致。比如StatementHandler.prepare方法的参数是(Connection, Integer),写成(Connection, int)就会报错:
java
// 错误示例:Integer写成int
@Signature(args = {Connection.class, int.class})
// 正确写法:
@Signature(args = {Connection.class, Integer.class})
解决:用 IDE 看目标方法的参数类型,严格复制过来。
- 拦截器里做复杂操作导致性能暴跌
反射遍历字段(比如脱敏拦截器)、频繁创建对象会拖慢 SQL 执行。
优化:
-
缓存反射获取的 Class 和 Field 信息(用ConcurrentHashMap存)
-
非必要不拦截(比如只拦查询,不拦更新)
-
复杂逻辑异步处理(比如慢查询日志用异步线程打印)