MyBatis 拦截器让搞定监控、脱敏和权限控制

一、先搞懂:MyBatis 拦截器到底能干嘛?

MyBatis 的拦截器本质是JDK 动态代理,能在 SQL 执行的关键节点 "插队" 执行我们的逻辑。它能拦截 4 个核心组件,覆盖 SQL 从生成到结果返回的全流程:

简单说:想监控 SQL 执行时间,拦Executor;想改 SQL(比如加租户条件),拦StatementHandler;想脱敏返回结果,拦ResultSetHandler。

拦截器必须实现Interceptor接口,核心就 3 个方法:

java 复制代码
public interface Interceptor {
    // 核心:拦截逻辑写这里
    Object intercept(Invocation invocation) throws Throwable;
    
    // 生成代理对象(直接用Plugin.wrap()就行)
    Object plugin(Object target);
    
    // 读取配置参数(比如从配置文件拿慢查询阈值)
    void setProperties(Properties properties);
}

还要用@Intercepts和@Signature注解告诉 MyBatis 要拦谁、拦哪个方法:

java 复制代码
// 示例:拦截StatementHandler的prepare方法(SQL预编译阶段)
@Intercepts({
    @Signature(
        type = StatementHandler.class, // 拦截哪个组件
        method = "prepare", // 拦截组件的哪个方法
        args = {Connection.class, Integer.class} // 方法参数类型(必须严格匹配)
    )
})
public class MySqlInterceptor implements Interceptor {
    // 实现接口方法...
}

踩坑提醒:args参数必须和方法实际参数类型完全一致!比如prepare方法有两个重载,记错参数类型就会拦截失败。

二、实战一:慢查询监控拦截器(一行代码定位超时 SQL)

需求:自动记录所有 SQL 的执行时间,超过 500ms 就报警,包含完整 SQL 和参数。实现步骤:

  1. 写拦截器逻辑(拦 Executor 的 query 和 update 方法)
java 复制代码
@Slf4j
@Intercepts({
    // 拦截查询方法
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    ),
    // 拦截增删改方法
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    )
})
public class SlowSqlInterceptor implements Interceptor {
    // 慢查询阈值(默认500ms,可配置)
    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和参数
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            String sql = mappedStatement.getBoundSql(parameter).getSql(); // 带?的SQL
            
            // 5. 慢查询报警
            if (costTime > slowThreshold) {
                log.warn("[慢查询警告] 耗时: {}ms, SQL: {}, 参数: {}", costTime, sql, parameter);
            } 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) {
            this.slowThreshold = Long.parseLong(threshold);
        }
    }
}
  1. 注册拦截器到 SpringBoot
java 复制代码
@Configuration
@MapperScan("com.example.mapper")// 扫描Mapper接口
public class MyBatisConfig {
    // 注册慢查询拦截器
    @Bean
    public SlowSqlInterceptor slowSqlInterceptor() {
        SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
        // 配置阈值(也可以在application.yml里配)
        Properties props = new Properties();
        props.setProperty("slowThreshold", "500"); // 500ms
        interceptor.setProperties(props);
        return interceptor;
    }

    // 把拦截器加到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();
    }
}
  1. 测试效果

调用任意查询接口,控制台会自动打印:

java 复制代码
[SQL监控] 耗时: 32ms, SQL: SELECT id,username,phone FROM user WHERE id = ? 

如果 SQL 执行超过 500ms(比如查大数据量表):

java 复制代码
[慢查询警告] 耗时: 1200ms, SQL: SELECT * FROM order WHERE user_id = ?, 参数: 10086 

关键优势:不用改任何 Service 或 Mapper 代码,所有 SQL 自动被监控。

三、实战二:数据脱敏拦截器(手机号、身份证自动打码)

需求:查询用户信息时,自动把手机号(13812345678→1385678)、身份证号(110...1234→************34)脱敏。

实现步骤:

  1. 自定义脱敏注解(标记需要脱敏的字段)
java 复制代码
// 作用在字段上的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType type(); // 脱敏类型(手机号/身份证)
}

// 脱敏类型枚举
public enum SensitiveType {
    PHONE, ID_CARD
}
  1. 实体类加注解(告诉哪些字段要脱敏)
java 复制代码
@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;
}
  1. 脱敏工具类(实现具体打码逻辑)
java 复制代码
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");
    }
}
  1. 写结果集拦截器(拦 ResultSetHandler 处理返回结果)
java 复制代码
@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) {
                desensitize(obj); // 脱敏处理
            }
        }
        return result;
    }

    // 反射处理对象中的敏感字段
    private void desensitize(Object obj) throws IllegalAccessException {
        if (obj == null) return;

        Class<?> clazz = obj.getClass();
        // 遍历所有字段
        for (Field field : clazz.getDeclaredFields()) {
            // 3. 检查字段是否有@Sensitive注解
            if (field.isAnnotationPresent(Sensitive.class)) {
                Sensitive annotation = field.getAnnotation(Sensitive.class);
                field.setAccessible(true); // 允许访问私有字段
                Object value = field.get(obj); // 获取字段值

                // 4. 根据类型脱敏
                if (value instanceof String) {
                    String strValue = (String) value;
                    switch (annotation.type()) {
                        case PHONE:
                            field.set(obj, SensitiveUtils.maskPhone(strValue));
                            break;
                        case ID_CARD:
                            field.set(obj, SensitiveUtils.maskIdCard(strValue));
                            break;
                    }
                }
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}
  1. 注册多个拦截器(注意顺序!)
java 复制代码
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
    // 慢查询拦截器(先注册)
    @Bean
    public SlowSqlInterceptor slowSqlInterceptor() { ... }

    // 脱敏拦截器(后注册)
    @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);
        // 按执行顺序添加:先监控SQL,再处理结果
        sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);
        return sessionFactory.getObject();
    }
}
  1. 测试效果
    查询用户后返回的对象自动脱敏:
java 复制代码
User user = userService.getById(1L);
System.out.println(user); 
// 输出:User(id=1, username=张三, phone=138****5678, idCard=****************34)

三、避坑指南:这 3 个错误 90% 的人都会犯

  1. 拦截器顺序搞反

多个拦截器时,注册顺序就是执行顺序。比如先注册脱敏拦截器,再注册慢查询拦截器,会导致慢查询日志里的参数已经被脱敏,排查问题时看不到原始值。

正确顺序:按 "SQL 执行前→执行中→执行后" 排序,比如:权限拦截器(改 SQL)→慢查询拦截器(监控)→脱敏拦截器(处理结果)。

  1. @Signature 的 args 参数写错

拦截不到方法大概率是因为args参数类型和目标方法不一致。比如StatementHandler.prepare方法的参数是(Connection, Integer),写成(Connection, int)就会报错:

java 复制代码
// 错误示例:Integer写成int
@Signature(args = {Connection.class, int.class})
// 正确写法:
@Signature(args = {Connection.class, Integer.class})

解决:用 IDE 看目标方法的参数类型,严格复制过来。

  1. 拦截器里做复杂操作导致性能暴跌

反射遍历字段(比如脱敏拦截器)、频繁创建对象会拖慢 SQL 执行。

优化:

  • 缓存反射获取的 Class 和 Field 信息(用ConcurrentHashMap存)

  • 非必要不拦截(比如只拦查询,不拦更新)

  • 复杂逻辑异步处理(比如慢查询日志用异步线程打印)

相关推荐
╭╰4023 小时前
苍穹外卖优化-续
java·spring·mybatis
weixin_456904274 小时前
基于Spring Boot + MyBatis的用户管理系统配置
spring boot·后端·mybatis
码熔burning11 小时前
Redis 的三种高效缓存读写策略!
redis·缓存·mybatis
stein_java21 小时前
Mybatis-7 XML映射器
数据库·sql·mybatis
托比-马奎尔1 天前
MyBatis入门到精通:CRUD实战指南
mybatis
ByteBlossom1 天前
MyBatis高频问题-自动映射与缓存解析
mybatis
半夏陌离1 天前
SQL 进阶指南:视图的创建与使用(视图语法 / 作用 / 权限控制)
java·数据库·mybatis
BillKu1 天前
Spring Boot中MyBatis的定义与使用
spring boot·mybatis
lozhyf1 天前
固定资产管理系统(vue+Springboot+mybatis)
vue.js·spring boot·mybatis