• MyBatis 的 SQL 拦截器:原理、实现与实践

MyBatis 的 SQL 拦截器:原理、实现与实践

2025-08-16 12:33:00 栏目:宝塔面板 36 阅读

前言

在MyBatis框架的使用过程中,我们常常需要对SQL执行过程进行干预 —— 比如打印执行日志、统计执行时间、动态修改SQL语句,甚至实现数据权限控制。而MyBatis提供的SQL拦截器(Interceptor)机制,正是实现这些需求的核心工具。

核心原理

MyBatis的SQL拦截器本质上是基于JDK动态代理实现的插件机制,它允许开发者在 SQL 执行的关键节点插入自定义逻辑。要理解其原理,需先明确两个核心概念:拦截目标与代理机制。

核心接口
  • Executor:MyBatis的核心执行器,负责SQL的整体执行(如select、update、commit等),是最常用的拦截目标。
  • StatementHandler:处理SQL语句的准备(如创建 Statement)、参数设置、结果集映射等,可用于修改SQL语句或参数。
  • ParameterHandler:处理SQL参数的设置(如为PreparedStatement设置参数),适合拦截参数并进行加工。
  • ResultSetHandler:处理查询结果集的映射(如将结果映射为Java对象),可用于修改返回结果。
代理机制

MyBatis的拦截器通过动态代理 + 责任链模式工作:当定义一个拦截器后,MyBatis会为被拦截的接口生成代理对象,将拦截逻辑嵌入代理对象中;若存在多个拦截器,则会形成代理链(外层代理调用内层代理,最终调用原始对象)。 具体流程如下:

  • 拦截器通过@Intercepts注解声明拦截目标(接口、方法、参数);
  • MyBatise 启动时扫描拦截器,为目标接口创建代理对象;
  • 当调用目标接口的方法时,代理对象先执行拦截器的intercept方法(自定义逻辑),再调用原始方法;
  • 若有多个拦截器,代理对象会按顺序执行所有拦截逻辑后,再执行原始方法。

实现步骤

实现一个MyBatis SQL拦截器需遵循固定流程:定义拦截器类、声明拦截目标、实现拦截逻辑,最后配置生效。下面以SQL 执行时间统计为例,详解具体实现。

定义拦截器类:实现 Interceptor 接口

该接口包含3个核心方法:

  • intercept(Invocation invocation):核心方法,拦截逻辑的实现(如统计时间、修改参数)。
  • plugin(Object target):决定是否为目标对象生成代理(通常通过Plugin.wrap(target, this)实现)。
  • setProperties(Properties properties):接收配置文件中传入的参数(如拦截器开关、日志级别)。
// 声明拦截目标:拦截Executor的query和update方法
@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 SqlExecuteTimeInterceptor implements Interceptor {

    // 拦截逻辑:统计SQL执行时间
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 记录开始时间
        long startTime = System.currentTimeMillis();
        try {
            // 2. 执行原始方法(如query/update)
            return invocation.proceed();
        } finally {
            // 3. 计算执行时间并打印
            long endTime = System.currentTimeMillis();
            long cost = endTime - startTime;
            // 获取SQL语句(从MappedStatement中提取)
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            String sqlId = mappedStatement.getId(); // Mapper接口方法全路径
            System.out.printf("SQL执行:%s,耗时:%d ms%n", sqlId, cost);
        }
    }

    // 生成代理对象(固定写法)
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 接收配置参数(如无需参数可空实现)
    @Override
    public void setProperties(Properties properties) {
        // 例如:从配置中获取阈值,超过阈值打印警告
        String threshold = properties.getProperty("slowSqlThreshold");
        if (threshold != null) {
            // 处理参数...
        }
    }
}

声明拦截目标:@Intercepts 与 @Signature

拦截器必须通过@Intercepts和@Signature注解明确拦截目标,否则MyBatis无法识别拦截逻辑。

  • @Intercepts:包裹一个或多个@Signature,表示拦截的一组目标。
  • @Signature:定义单个拦截目标,包含3个属性:

type:被拦截的接口(如Executor、StatementHandler);

method:被拦截的方法名(如Executor的query、update);

args:被拦截方法的参数类型数组(需与接口方法参数完全一致,用于区分重载方法)。

配置拦截器:让 MyBatis 识别拦截器

方式 1:MyBatis 原生配置(mybatis-config.xml)

  
    
    
      
       
    
  
方式 2:Spring Boot 配置(通过 @Bean 注册)
@Configuration
public class MyBatisConfig {
    @Bean
    public SqlExecuteTimeInterceptor sqlExecuteTimeInterceptor() {
        SqlExecuteTimeInterceptor interceptor = new SqlExecuteTimeInterceptor();
        // 设置参数
        Properties properties = new Properties();
        properties.setProperty("slowSqlThreshold", "500");
        interceptor.setProperties(properties);
        return interceptor;
    }
}

实战案例

动态修改 SQL(如数据权限控制)

对多租户系统,自动在SQL中添加租户ID条件(如where tenant_id = 123),避免手动编写。

@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 获取StatementHandler及原始SQL
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
    // 获取当前租户ID(从ThreadLocal或登录上下文获取)
    String tenantId = TenantContext.getCurrentTenantId(); // 自定义上下文类

    // 拼接租户条件(简单示例:仅对SELECT语句处理)
    if (originalSql.trim().toLowerCase().startsWith("select") && tenantId != null) {
        String modifiedSql = originalSql + " and tenant_id = " + tenantId;
        // 修改SQL
        metaObject.setValue("delegate.boundSql.sql", modifiedSql);
    }

    return invocation.proceed(); // 执行修改后的SQL
}
参数加密与解密

对敏感参数(如手机号、身份证号)在入库前加密,查询时解密。

@Override
public Object intercept(Invocation invocation) throws Throwable {
    ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(parameterHandler);
    // 获取参数对象(如User对象)
    Object parameter = metaObject.getValue("parameterObject");
    if (parameter instanceof User) {
        User user = (User) parameter;
        // 加密手机号
        if (user.getPhone() != null) {
            user.setPhone(EncryptUtil.encrypt(user.getPhone())); // 自定义加密工具
        }
    }
    return invocation.proceed(); // 执行参数设置
}

注意事项

避免过度拦截,控制拦截范围

拦截器会嵌入SQL执行流程,过多或过频繁的拦截会增加性能开销(尤其是query、prepare等高频方法)。建议:

  • 仅拦截必要的接口和方法(如统计时间用Executor,改SQL用StatementHandler);
  • 避免在拦截逻辑中执行耗时操作(如IO、复杂计算)。

处理代理对象:获取原始对象

由于MyBatis会对目标接口生成代理,直接调用invocation.getTarget()可能得到代理对象(而非原始对象),需通过反射或MetaObject获取原始对象(如StatementHandler的delegate属性)。

推荐使用MyBatis提供的SystemMetaObject工具类处理反射,避免手动编写反射代码:

MetaObject metaObject = SystemMetaObject.forObject(target);
// 获取原始StatementHandler(delegate为StatementHandler代理的原始对象)
Object originalHandler = metaObject.getValue("delegate");

控制拦截器顺序:@Order 或配置顺序

若存在多个拦截器,执行顺序由注册顺序决定(先注册的先执行)。在Spring环境中,可通过@Order注解指定顺序(值越小越先执行):

@Order(1) // 第一个执行
public class SqlLogInterceptor implements Interceptor { ... }

@Order(2) // 第二个执行
public class SqlModifyInterceptor implements Interceptor { ... }

总结

MyBatis的SQL拦截器是其插件机制的核心,通过动态代理实现对SQL执行过程的灵活干预。本文从原理(四大接口、动态代理)、实现(定义拦截器、声明目标、配置生效)到实践(日志统计、SQL修改、参数加密),全面解析了拦截器的使用。

合理使用拦截器可以简化代码(如自动添加租户条件)、增强可观测性(如SQL日志),但需注意性能与兼容性。

本文地址:https://www.yitenyun.com/331.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL Web 应用 异步数据库 数据同步 ACK 双主架构 循环复制 TIME_WAIT 运维 负载均衡 JumpServer SSL 堡垒机 跳板机 HTTPS JumpServer安装 堡垒机安装 Linux安装JumpServer Deepseek 宝塔面板 Linux宝塔 Docker 生命周期 esxi esxi6 root密码不对 无法登录 web无法登录 HexHub 序列 核心机制 Windows Windows server net3.5 .NET 安装出错 服务器 管理口 HTTPS加密 宝塔面板打不开 宝塔面板无法访问 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 Windows宝塔 Mysql重置密码 Oracle 处理机制 InnoDB 数据库锁 无法访问宝塔面板 开源 PostgreSQL 存储引擎 监控 连接控制 机制 Serverless 无服务器 语言 ES 技术 服务器性能 协同 Spring Redis 异步化 缓存方案 缓存架构 缓存穿透 分页查询 索引 高可用 group by Undo Log GreatSQL 连接数 SQL 动态查询 机器学习 日志文件 MIXED 3 响应模型 R2DBC SVM Embedding 优化 万能公式 R edis 线程 数据 主库 RocketMQ 长轮询 配置 自定义序列化 Linux 安全 ​Redis 推荐模型 Postgres OTel Iceberg 工具 云原生 Netstat Linux 服务器 端口 scp Linux的scp怎么用 scp上传 scp下载 scp命令 AI 助手 SQLark 向量数据库 大模型 共享锁 PG DBA 存储 SQLite-Web SQLite 数据库管理工具 openHalo Hash 字段 Recursive OB 单机版 查询 电商 系统 Ftp 架构 流量 Rsync 锁机制 • 索引 • 数据库 修改DNS Centos7如何修改DNS redo log 重做日志 数据分类 加密 磁盘架构 人工智能 推荐系统 场景 聚簇 非聚簇 sftp 服务器 参数 向量库 Milvus 线上 库存 预扣 业务 同城 双活 信息化 智能运维 MySQL 9.3 防火墙 黑客 Doris SeaTunnel MVCC Python 高效统计 今天这篇文章就跟大家 不宕机 数据备份 传统数据库 向量化 缓存 mini-redis INCR指令 网络架构 网络配置 INSERT COMPACT 分库 分表 窗口 函数 Redisson 锁芯 RDB AOF prometheus Alert PostGIS 启动故障 事务 Java 开发 Canal Web IT运维 崖山 新版本 filelock MongoDB 数据结构 核心架构 订阅机制 引擎 性能 数据脱敏 加密算法 B+Tree ID 字段 分布式 集中式 ZODB Go 数据库迁移 读写 容器 数据类型 虚拟服务器 虚拟机 内存 容器化 网络故障 DBMS 管理系统 频繁 Codis 模型 Redis 8.0 OAuth2 Token JOIN 微软 SQL Server AI功能 QPS 高并发 国产数据库 发件箱模式 自动重启 Pottery 聚簇索引 非聚簇索引 原子性 工具链 部署 Entity Testcloud 云端自动化 速度 服务器中毒 事务隔离 SpringAI 排行榜 排序 SSH Caffeine CP Web 接口 分页方案 排版 数据集成工具 MCP 开放协议 行业 趋势 数据页 Redka 悲观锁 乐观锁 StarRocks 数据仓库 sqlmock LRU 大表 业务场景 分页 AIOPS 分布式架构 分布式锁​ 1 优化器 池化技术 连接池 单点故障 仪表盘 网络 dbt 数据转换工具 Order EasyExcel MySQL8 意向锁 记录锁 事务同步 InfluxDB 日志 IT 对象 字典 RAG HelixDB 双引擎 播客 订单 单线程 主从复制 代理 Crash 代码 编程 UUIDv7 主键 UUID ID Ansible LLM 语句 Pump 恢复数据 Valkey Valkey8.0 ReadView 产业链 兼容性 数据字典 线程安全 List 类型 Weaviate 失效 MGR 分布式集群 解锁 调优 Next-Key 表空间 分布式锁 Zookeeper 关系数据库 慢SQL优化 RR 互联网 GitHub Git 矢量存储 数据库类型 AI代理 查询规划 国产 用户 算法 快照读 当前读 视图 千万级 神经系统 count(*) count(主键) 行数 CAS 技巧 拦截器 动态代理 多线程 并发控制 恢复机制 闪回