SpringBoot集成MyBatis的SQL拦截器实战
2026/6/5 1:54:53 网站建设 项目流程

一、为什么需要SQL拦截器?

慢查询监控:生产环境突然出现接口超时,需要快速定位执行时间过长的SQL

数据脱敏:用户表查询结果中的手机号、身份证号需要自动替换为****

权限控制:多租户系统中,自动给SQL添加tenant_id = ?条件,防止数据越权访问

SQL审计:记录所有执行的SQL语句 、执行人、执行时间,满足合规要求

如果没有拦截器,这些需求可能需要修改每一个Mapper接口或Service方法,工作量巨大。

而MyBatis的SQL拦截器能在SQL执行的各个阶段进行拦截处理,实现"无侵入式"增强。

二、MyBatis拦截器基础

2.1 核心接口:Interceptor

MyBatis的拦截器机制基于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 = ?, 参数: {id=1, param1=1}

踩坑提示如果拦截不到SQL,检查@Signature注解的args参数是否与方法参数类型完全匹配!

四、实战二:数据脱敏拦截器(敏感信息保护)

1、需求说明

查询用户信息时,自动将敏感字段脱敏:

  • 手机号:13812345678 → 138****5678

  • 身份证号:110101199001011234 → ****************34

2、完整实现代码

自定义脱敏注解

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(id=1, username=张三, phone=138****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监控和数据脱敏,避免了修改大量业务代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询