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

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

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

前言

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