开发者

SpringBoot集成MyBatis中SQL拦截器的实战指南

目录
  • 一、为什么需要SQL拦截器?
  • 二、MyBATis拦截器基础
    • 2.1 核心接口:Interceptor
    • 2.2 拦截目标与签名配置
  • 三、实战一:慢查询监控拦截器
    • 3.1 需求说明
    • 3.2 完整实现代码
  • 四、实战二:数据脱敏拦截器(敏感信息保护)
    • 4.1 需求说明
    • 4.2 完整实现代码
  • 五、实战踩坑指南
    • 5.1 拦截器顺序问题
    • 5.2 拦截器签名配置错误
    • 5.3 性能问题
  • 六、总结与扩展

    一、为什么需要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注解指定拦截目标:

    拦截类型作用常用拦截方法
    ExecutorSQL执行器(最常用)update、query、commit、rollback
    StatementHandlerSQL语句处理器(控制SQL生成)prepare、parameterize
    ParameterpythonHandler参数处理器(处理SQL参数)setParameters
    ResultSetHandler结果集处理器(处理查询结果)handleResultSets

    举个栗子:拦截StatementHandlerprepare方法(SQL预编译阶段):

    @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)拦截器类

    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.getTargetandroid();
                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注册拦截器

    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;
    
    www.devze.com@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 intjavascripterceptor;
        }
    
        @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)测试效果

    写个简单的查询接口:

    @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参数是否与方法参数类型完全匹配!

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

    4.1 需求说明

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

    • 手机号:13812345678138****5678
    • 身份证号:110101199001011234****************34

    4.2 完整实现代码

    (1)自定义脱敏注解

    import java.lang.annotation.*;
    
    // 作用在字段上
    @Target(ElementType.FIELD)
    // 运行时生效
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Sensitive {
        // 脱敏类型(手机号、身份证号等)
        SensitiveType type();
    }
    
    // 脱敏类型枚举
    public enum SensitiveType {
        PHONE,    // 手机号
        ID_CARD   // 身份证号
    }
    

    (2)实体类添加注解

    import lombok.Data;
    
    @Data
    public class User {
        private Long id;
        pwww.devze.comrivate String username;
        
        @Sensitive(type = SensitiveType.PHONE)  // 手机号脱敏
        private String phone;
        
        @Sensitive(type = SensitiveType.ID_CARD)  // 身份证号脱敏
        private String idCard;
    }
    

    (3)脱敏工具类

    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)结果集拦截器

    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,添加脱敏拦截器:

    @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)测试效果

    查询用户信息:

    User user = userService.getUserById(1L);
    System.out.println(user); 
    // 输出:User(id=1, username=张三, phone=138****5678, idCard=****************34)
    

    五、实战踩坑指南

    5.1 拦截器顺序问题

    :多个拦截器时,注册顺序就是执行顺序。比如先注册慢查询拦截器,再注册脱敏拦截器:

    SQL执行 → 慢查询拦截器(记录时间) → 脱敏拦截器(处理结果)

    如果顺序反了,脱敏拦截器会先处理结果,慢查询拦截器记录的SQL就看不到原始参数了。

    解决:按"执行SQL前→执行SQL后→处理结果"的顺序注册。

    5.2 拦截器签名配置错误

    @Signatureargs参数类型写错,导致拦截不到方法。比如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查看方法参数类型,确保完全一致。

    5.3 性能问题

    :在拦截器中做复杂操作(如反射遍历所有字段)会影响性能。

    解决

    • 反射操作缓存Class信息
    • 非必要不拦截(如只拦截查询方法)
    • 敏感字段脱敏可考虑在DTO层处理

    六、总结与扩展

    通过SQL拦截器,我们用极少的代码实现了SQL监控和数据脱敏,避免了修改大量业务代码。

    到此这篇关于SpringBoot集成MyBatis中SQL拦截器的实战指南的文章就介绍到这了,更多相关SpringBoot集成MyBatis SQL拦截器内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜