SpringBoot集成MyBatis的SQL拦截器实战

一、为什么需要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

举个栗子 :拦截StatementHandlerprepare方法(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 需求说明

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

  • 手机号:13812345678138****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 拦截器签名配置错误

@Signatureargs参数类型写错,导致拦截不到方法。比如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监控和数据脱敏,避免了修改大量业务代码。

相关推荐
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 分钟前
配置springdoc swagger开关
java
Echo flower6 分钟前
Spring Boot WebFlux 实现流式数据传输与断点续传
java·spring boot·后端
没有bug.的程序员12 分钟前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
鸽鸽程序猿17 分钟前
【Redis】Java客户端使用Redis
java·redis·github
悦悦子a啊17 分钟前
使用 Java 集合类中的 LinkedList 模拟栈以此判断字符串是否是回文
java·开发语言
Lucky小小吴19 分钟前
java代码审计入门篇——Hello-Java-Sec(完结)
java·开发语言
一个想打拳的程序员21 分钟前
无需复杂配置!用%20docker-webtop%20打造跨设备通用%20Linux%20桌面,加载cpolar远程访问就这么简单
java·人工智能·docker·容器
一起养小猫23 分钟前
LeetCode100天Day2-验证回文串与接雨水
java·leetcode
清晓粼溪27 分钟前
Java登录认证解决方案
java·开发语言
小徐Chao努力28 分钟前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang