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

前言

在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) { // 处理参数... } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.
声明拦截目标:@Intercepts 与 @Signature

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

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

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

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

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

配置拦截器:让 MyBatis 识别拦截器方式 1:MyBatis 原生配置(mybatis-config.xml)
复制
<configuration> <plugins> <!-- 配置SQL执行时间拦截器 --> <plugin interceptor="com.example.SqlExecuteTimeInterceptor"> <!-- 可选:传入参数(对应setProperties方法) --> <property name="slowSqlThreshold" value="500"/> <!-- 慢SQL阈值:500ms --> </plugin> </plugins> </configuration>1.2.3.4.5.6.7.8.9.
方式 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; } }1.2.3.4.5.6.7.8.9.10.11.12.
实战案例动态修改 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 }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.
参数加密与解密

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

复制
@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(); // 执行参数设置 }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

注意事项

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

拦截器会嵌入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");1.2.3.
控制拦截器顺序:@Order 或配置顺序

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

复制
@Order(1) // 第一个执行 public class SqlLogInterceptor implements Interceptor { ... } @Order(2) // 第二个执行 public class SqlModifyInterceptor implements Interceptor { ... }1.2.3.4.5.

总结

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

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

阅读剩余
THE END