一、为什么需要SQL拦截器?
先看几个真实场景:
慢查询监控 :生产环境突然出现接口超时,需要快速定位执行时间过长的SQL 数据脱敏 :用户表查询结果中的手机号、身份证号需要自动替换为****
权限控制 :多租户系统中,自动给SQL添加tenant_id = ?
条件,防止数据越权访问 SQL审计:记录所有执行的SQL语句、执行人、执行时间,满足合规要求
如果没有拦截器,这些需求可能需要修改每一个Mapper接口或Service方法,工作量巨大。
而MyBatis的SQL拦截器能在SQL执行的各个阶段进行拦截处理,实现"无侵入式"增强。
二、MyBatis拦截器基础
2.1 核心接口:Interceptor
MyBatis的拦截器机制基于JDK动态代理,所有自定义拦截器都要实现Interceptor
接口:
java
public interface Interceptor {
// 拦截逻辑的核心方法
Object intercept(Invocation invocation) throws Throwable;
// 生成代理对象(通常直接用Plugin.wrap())
Object plugin(Object target);
// 读取配置参数(如从mybatis-config.xml中获取)
void setProperties(Properties properties);
}
2.2 拦截目标与签名配置
MyBatis允许拦截4个核心组件的方法,通过@Intercepts
和@Signature
注解指定拦截目标:
拦截类型 | 作用 | 常用拦截方法 |
---|---|---|
Executor |
SQL执行器(最常用) | update、query、commit、rollback |
StatementHandler |
SQL语句处理器(控制SQL生成) | prepare、parameterize |
ParameterHandler |
参数处理器(处理SQL参数) | setParameters |
ResultSetHandler |
结果集处理器(处理查询结果) | handleResultSets |
举个栗子 :拦截StatementHandler
的prepare
方法(SQL预编译阶段):
less
@Intercepts({
@Signature(
type = StatementHandler.class, // 拦截哪个接口
method = "prepare", // 拦截接口的哪个方法
args = {Connection.class, Integer.class} // 方法参数类型(用于确定重载方法)
)
})
public class MySqlInterceptor implements Interceptor {
// 实现接口方法...
}
注意 :args参数必须严格匹配方法的参数类型,否则拦截不到!比如prepare
方法有两个重载,这里指定(Connection, Integer)
类型的参数。
三、实战一:慢查询监控拦截器
3.1 需求说明
监控所有SQL执行时间,超过阈值(如500ms)则打印警告日志,包含:
- SQL执行时间
- 完整SQL语句(带参数占位符)
- 参数值(防止SQL注入排查)
3.2 完整实现代码
(1)拦截器类
java
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;
@Slf4j
@Intercepts({
// 拦截查询方法
@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class}
),
// 拦截更新方法(insert/update/delete)
@Signature(
type = StatementHandler.class,
method = "update",
args = {Statement.class}
)
})
public class SlowSqlInterceptor implements Interceptor {
// 慢查询阈值(毫秒),可通过配置文件注入
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语句和参数
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String sql = statementHandler.getBoundSql().getSql(); // 获取SQL语句(带?占位符)
Object parameterObject = statementHandler.getBoundSql().getParameterObject(); // 获取参数
// 5. 判断是否慢查询
if (costTime > slowThreshold) {
log.warn("[慢查询警告] 执行时间: {}ms, SQL: {}, 参数: {}",
costTime, sql, parameterObject);
} 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) {
slowThreshold = Long.parseLong(threshold);
}
}
}
(2)SpringBoot注册拦截器
java
package com.example.config;
import com.example.interceptor.SensitiveInterceptor;
import com.example.interceptor.SlowSqlInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@MapperScan("com.example.mapper") // Mapper接口所在包
public class MyBatisConfig {
// 注册慢查询拦截器
@Bean
public SlowSqlInterceptor slowSqlInterceptor() {
SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
// 设置属性(也可通过application.yml配置)
Properties properties = new Properties();
properties.setProperty("slowThreshold", "500"); // 慢查询阈值500ms
interceptor.setProperties(properties);
return interceptor;
}
@Bean
public SensitiveInterceptor sensitiveInterceptor() {
return new SensitiveInterceptor();
}
// 将拦截器添加到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();
}
}
(3)测试效果
写个简单的查询接口:
kotlin
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
执行后控制台输出:
sql
[SQL监控] 执行时间: 30ms, SQL: SELECT id,username,phone FROM user WHERE id = ?
如果SQL执行时间超过500ms(比如查询大数据量表):
sql
[慢查询警告] 执行时间: 1430ms, SQL: SELECT * FROM user WHERE id = ?, 参数: {id=1, param1=1}
踩坑提示 :如果拦截不到SQL,检查@Signature
注解的args
参数是否与方法参数类型完全匹配!
四、实战二:数据脱敏拦截器(敏感信息保护)
4.1 需求说明
查询用户信息时,自动将敏感字段脱敏:
- 手机号:
13812345678
→138****5678
- 身份证号:
110101199001011234
→****************34
4.2 完整实现代码
(1)自定义脱敏注解
java
import java.lang.annotation.*;
// 作用在字段上
@Target(ElementType.FIELD)
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
// 脱敏类型(手机号、身份证号等)
SensitiveType type();
}
// 脱敏类型枚举
public enum SensitiveType {
PHONE, // 手机号
ID_CARD // 身份证号
}
(2)实体类添加注解
typescript
import lombok.Data;
@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;
}
(3)脱敏工具类
typescript
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");
}
}
(4)结果集拦截器
java
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
@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) {
// 3. 对有@Sensitive注解的字段进行脱敏
desensitize(obj);
}
}
return result;
}
// 反射处理对象中的敏感字段
private void desensitize(Object obj) throws IllegalAccessException {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields(); // 获取所有字段(包括私有)
for (Field field : fields) {
// 4. 检查字段是否有@Sensitive注解
if (field.isAnnotationPresent(Sensitive.class)) {
Sensitive annotation = field.getAnnotation(Sensitive.class);
field.setAccessible(true); // 开启私有字段访问权限
Object value = field.get(obj); // 获取字段值
if (value instanceof String) {
String strValue = (String) value;
// 5. 根据脱敏类型处理
switch (annotation.type()) {
case PHONE:
field.set(obj, SensitiveUtils.maskPhone(strValue));
break;
case ID_CARD:
field.set(obj, SensitiveUtils.maskIdCard(strValue));
break;
default:
break;
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可配置更多脱敏规则,此处省略
}
}
(5)注册多个拦截器
修改MyBatisConfig
,添加脱敏拦截器:
java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// ... 慢查询拦截器配置 ...
@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);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
// 注册多个拦截器(注意顺序!先执行的拦截器先注册)
sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);
return sessionFactory.getObject();
}
}
(6)测试效果
查询用户信息:
ini
User user = userService.getUserById(1L);
System.out.println(user);
// 输出:User(id=1, username=张三, phone=138****5678, idCard=****************34)
五、实战踩坑指南
5.1 拦截器顺序问题
坑:多个拦截器时,注册顺序就是执行顺序。比如先注册慢查询拦截器,再注册脱敏拦截器:
sql
SQL执行 → 慢查询拦截器(记录时间) → 脱敏拦截器(处理结果)
如果顺序反了,脱敏拦截器会先处理结果,慢查询拦截器记录的SQL就看不到原始参数了。
解决:按"执行SQL前→执行SQL后→处理结果"的顺序注册。
5.2 拦截器签名配置错误
坑 :@Signature
的args
参数类型写错,导致拦截不到方法。比如StatementHandler.prepare
方法有两个重载:
scss
// 正确的参数类型
prepare(Connection connection, Integer transactionTimeout)
// 错误示例:写成了(int)
@Signature(args = {Connection.class, int.class}) // 出现下面的异常!
java.lang.NoSuchMethodException: org.apache.ibatis.executor.statement.StatementHandler.prepare(java.sql.Connection,int)
解决:通过IDE查看方法参数类型,确保完全一致。
5.3 性能问题
坑:在拦截器中做复杂操作(如反射遍历所有字段)会影响性能。
解决:
- 反射操作缓存Class信息
- 非必要不拦截(如只拦截查询方法)
- 敏感字段脱敏可考虑在DTO层处理
六、总结与扩展
通过SQL拦截器,我们用极少的代码实现了SQL监控和数据脱敏,避免了修改大量业务代码。