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存)

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

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

相关推荐
wljt10 小时前
公司二-面试题
java·开发语言·mybatis
VimorphTech13 小时前
文本已经改了,旧的音频生成结果还能回写吗?
java·数据库·mybatis
麻瓜code18 小时前
双写一致性:为什么是“删缓存“,而不是“改缓存“
java·缓存·mybatis
郭小李2 天前
mybatis 动态 sql
java·sql·mybatis
知守观2 天前
技术考古:iBATIS 2.3.4 时代的持久层,是如何用 Map 玩转 307 个存储过程的?
java·后端·mybatis
captain3762 天前
MyBatis Plus(1)
java·服务器·mybatis
代码不停2 天前
MyBatis进阶
spring boot·mybatis
captain3762 天前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
captain3762 天前
MyBatis(进阶1)
mybatis
郭小李3 天前
mybatis的 Mapper.xml语法
xml·java·mybatis