
一、为什么需要SQL拦截器慢查询监控生产环境突然出现接口超时需要快速定位执行时间过长的SQL数据脱敏用户表查询结果中的手机号、身份证号需要自动替换为****权限控制多租户系统中自动给SQL添加tenant_id ?条件防止数据越权访问SQL审计记录所有执行的SQL语句 、执行人、执行时间满足合规要求如果没有拦截器这些需求可能需要修改每一个Mapper接口或Service方法工作量巨大。而MyBatis的SQL拦截器能在SQL执行的各个阶段进行拦截处理实现无侵入式增强。二、MyBatis拦截器基础2.1 核心接口InterceptorMyBatis的拦截器机制基于JDK动态代理所有自定义拦截器都要实现Interceptor接口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预编译阶段Intercepts({ Signature( type StatementHandler.class, // 拦截哪个接口 method prepare, // 拦截接口的哪个方法 args {Connection.class, Integer.class} // 方法参数类型用于确定重载方法 ) }) public class MySqlInterceptor implements Interceptor { // 实现接口方法... }注意args参数必须严格匹配方法的参数类型否则拦截不到比如prepare方法有两个重载这里指定(Connection, Integer)类型的参数三、实战一慢查询监控拦截器1、需求说明监控所有SQL执行时间超过阈值如500ms则打印警告日志包含SQL执行时间完整SQL语句带参数占位符参数值防止SQL注入排查2、完整实现代码拦截器类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); } } }SpringBoot注册拦截器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(); } }测试效果Service public class UserService { Autowired private UserMapper userMapper; public User getUserById(Long id) { return userMapper.selectById(id); } }执行后控制台输出[SQL监控] 执行时间: 30ms, SQL: SELECT id,username,phone FROM user WHERE id ?如果SQL执行时间超过500ms比如查询大数据量表[慢查询警告] 执行时间: 1430ms, SQL: SELECT * FROM user WHERE id ?, 参数: {id1, param11}踩坑提示如果拦截不到SQL检查Signature注解的args参数是否与方法参数类型完全匹配四、实战二数据脱敏拦截器敏感信息保护1、需求说明查询用户信息时自动将敏感字段脱敏手机号13812345678 → 138****5678身份证号110101199001011234 → ****************342、完整实现代码自定义脱敏注解import java.lang.annotation.*; // 作用在字段上 Target(ElementType.FIELD) // 运行时生效 Retention(RetentionPolicy.RUNTIME) public interface Sensitive { // 脱敏类型手机号、身份证号等 SensitiveType type(); } // 脱敏类型枚举 public enum SensitiveType { PHONE, // 手机号 ID_CARD // 身份证号 }实体类 添加注解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; }脱敏工具类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); } }结果集拦截器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) { // 可配置更多脱敏规则此处省略 } }注册多个拦截器修改MyBatisConfig添加脱敏拦截器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(); } }测试效果查询用户信息User user userService.getUserById(1L); System.out.println(user); // 输出User(id1, username张三, phone138****5678, idCard****************34)五、实战踩坑指南1、拦截器顺序问题坑多个拦截器时注册顺序就是执行顺序。比如先注册慢查询拦截器再注册脱敏拦截器SQL执行 → 慢查询拦截器记录时间 → 脱敏拦截器处理结果如果顺序反了脱敏拦截器会先处理结果慢查询拦截器记录的SQL就看不到原始参数了。解决按执行SQL前→执行SQL后→处理结果的顺序注册。2、拦截器签名配置错误坑Signature的args参数类型写错导致拦截不到方法。比如StatementHandler.prepare方法有两个重载// 正确的参数类型 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查看方法参数类型确保完全一致。3、性能问题坑在拦截器中做复杂操作如反射 遍历所有字段会影响性能。解决反射操作缓存Class信息非必要不拦截如只拦截查询方法敏感字段脱敏可考虑在DTO层处理六、总结与扩展通过SQL拦截器我们用极少的代码实现了SQL监控和数据脱敏避免了修改大量业务代码。