Mybatis 拦截器修改 SQL:面试官最爱问的实现原理与实战场景
大家好,我是经常和 Mybatis 打交道的程序员。今天咱们来深入聊聊 Mybatis 里一个非常强大且面试高频的特性——拦截器(Interceptor),特别是如何用它来修改 SQL。面试官特别喜欢问这个,因为它能考察你对 Mybatis 框架执行过程的理解深度和实际应用能力。咱们就以面试中口述回答的思路来拆解它。
H2 Mybatis 拦截器到底是个啥?它能干啥?
说白了,Mybatis 拦截器就像是在 Mybatis 执行 SQL 语句这条流水线上安插的“关卡”。它基于 JDK 动态代理实现,允许你在 Mybatis 核心对象(比如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法执行前后“横插一脚”,干点自定义的事情。
它能干的活可多了:
- 监控和统计 SQL 执行性能:记录每个 SQL 的执行时间。
- 分页功能实现:很多分页插件底层就是靠它拦截
SQL并修改(加上limit、offset或改写为数据库特定的分页语句)。 - 数据权限控制:根据当前用户角色,自动在
SQL的WHERE条件里拼接数据过滤条件。 - SQL 改写:比如统一给表名加前缀/后缀、替换特定的 SQL 片段、实现逻辑删除(自动将
delete转update ... set deleted=1)等。这就是我们今天要重点讲的“修改 SQL”的核心应用! - 参数和结果的加解密:在设置参数前加密,在获取结果后解密。
- 多租户数据隔离:自动在
SQL中加上租户ID过滤条件。
所以,当你听到面试官问“Mybatis 拦截器怎么用?能做什么?”,或者更具体的“如何用拦截器动态修改 SQL?”,你就知道该聊这个了。
H2 修改 SQL 的关键:拦截 StatementHandler
想在 Mybatis 里修改即将执行的 SQL,拦截哪个接口是关键! 答案是:org.apache.ibatis.executor.statement.StatementHandler。为啥呢?因为它主要负责创建 Statement 对象、预编译带占位符的 SQL(prepare 方法)、以及执行 SQL。
核心步骤拆解:
-
创建你的拦截器类:
- 实现
org.apache.ibatis.plugin.Interceptor接口。 - 核心是重写
intercept(Invocation invocation)方法,在这里写你的拦截逻辑。 - 重写
plugin(Object target)方法,通常用Plugin.wrap(target, this)返回代理对象。 - 重写
setProperties(Properties properties)方法(可选),用于接收配置的参数。
- 实现
-
指定要拦截的方法:
- 在你的拦截器类上使用
@Intercepts注解。 - 注解里包含一个或多个
@Signature注解。 - 为了修改 SQL,我们主要拦截
StatementHandler的prepare(Connection connection, Integer transactionTimeout)方法! 这个方法的参数connection就是数据库连接。这个方法执行后,原始的 SQL 语句(带#{}占位符)会被预编译成数据库认识的带?的语句。
@Intercepts({ @Signature(type = StatementHandler.class, // 拦截的目标接口 method = "prepare", // 拦截的方法名 args = {Connection.class, Integer.class}) // 方法的参数类型 }) public class MySqlModifyInterceptor implements Interceptor { // ... 实现 intercept 等方法 } - 在你的拦截器类上使用
-
在 intercept 方法里获取并修改 SQL:
intercept(Invocation invocation)方法的参数Invocation封装了被拦截的目标对象、方法及其参数。- 通过
invocation.getTarget()获取被拦截的StatementHandler对象。 - 通常我们需要向下转型为
RoutingStatementHandler或具体的实现类(如PreparedStatementHandler),然后获取其持有的BoundSql对象。BoundSql是核心! - 关键对象:
BoundSqlBoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();// 这就是我们要修改的原始 SQL 字符串!Object parameterObject = boundSql.getParameterObject();// 获取执行 SQL 时传入的参数对象(可选,用于动态修改逻辑)
- 修改 SQL: 拿到原始的
sql字符串后,你就可以利用字符串操作(替换、拼接等)或者Jsoup等工具进行复杂的解析和改写了。比如:String newSql = sql + " AND tenant_id = " + currentTenantId;(简单拼接,注意 SQL 注入风险!实践中会用占位符)String newSql = sql.replace("DELETE FROM", "UPDATE ... SET deleted=1 WHERE");(逻辑删除)String newSql = addPaginationLimit(sql, pageNum, pageSize);(分页)
- 反射修改 BoundSql 的 SQL: 由于
BoundSql中的sql字段通常没有提供setter方法,我们需要通过反射来修改它:Field sqlField = BoundSql.class.getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, newSql); // 将修改后的 newSql 设置回去! - 最后调用
invocation.proceed()继续执行链上的下一个拦截器或原方法。
-
注册拦截器:
- 在
Mybatis配置文件中(通常是mybatis-config.xml),在<plugins>标签下注册你的拦截器。 -
<plugins> <plugin interceptor="com.yourpackage.MySqlModifyInterceptor"> <!-- 这里可以传递 properties 参数,在 setProperties 方法中接收 --> <!-- <property name="someProperty" value="someValue"/> --> </plugin> </plugins>
- 在
H2 面试实战:如何回答“你用拦截器修改 SQL 的流程?”
“面试官您好,我使用 Mybatis 拦截器修改 SQL 的核心流程是这样的(结合上面步骤):”
- 明确目标: 我要拦截
StatementHandler接口的prepare方法,因为在这个阶段能拿到待执行的原始SQL(BoundSql)。 - 实现拦截器: 定义一个类实现
Interceptor接口,用@Intercepts和@Signature注解精确指定拦截点。 - 获取并操作 SQL:
- 在
intercept方法里,通过invocation.getTarget()拿到当前的StatementHandler。 - 通过
StatementHandler.getBoundSql()拿到关键的BoundSql对象。 - 调用
boundSql.getSql()获取原始的SQL字符串。 - 根据业务需求(比如数据权限、分页、逻辑删除、表名替换)对原始的
SQL字符串进行修改,得到新的SQL字符串 (newSql)。
- 在
- 反射设置新 SQL: 因为
BoundSql的sql字段没有setter,所以需要通过反射 (Field.setAccessible(true)+field.set()) 将newSql设置回BoundSql对象中。 - 继续执行: 调用
invocation.proceed()让流程继续。 - 配置生效: 在
Mybatis的全局配置文件 (mybatis-config.xml) 的<plugins>部分注册我的拦截器类。
“举个实际应用场景的例子:比如实现数据行级权限。我拦截了查询 SQL,在 WHERE 条件后面动态拼接上了 AND create_by = #{currentUserId}。这样,用户只能看到自己创建的数据。这里需要注意 SQL 注入问题,拼接时要确保值的安全性,通常会结合参数映射来处理。”
H2 注意事项与避坑指南
- 理解执行时机: 修改
SQL一定要在Statement被预编译 (prepare) 之前完成!拦截prepare方法是最合适的。 - 反射的风险: 修改
BoundSql的sql字段必须用反射,这依赖于Mybatis内部实现细节。虽然Mybatis本身对这个字段的使用比较稳定,但跨大版本升级时还是需要测试一下兼容性。面试时提一下这个点,能显示你思考的深度。 - 性能考虑:
SQL的字符串操作(尤其是复杂的正则替换或解析)会有性能开销。避免在拦截器里做太重的操作,特别是高频执行的简单 SQL。 - 谨慎拦截: 拦截器是全局生效的,要确保你的逻辑只影响需要修改的
SQL。可以通过BoundSql的SqlCommandType(SELECT, UPDATE, INSERT, DELETE) 或Mapper Id(boundSql.getSqlCommandType()/MappedStatement.getId()) 来精确判断哪些SQL需要被处理。 - SQL 注入: 如果你直接在
SQL字符串里拼接用户输入的值(而不是使用PreparedStatement的占位符?),极有可能引入 SQL 注入漏洞! 在数据权限等场景动态拼接条件时,强烈建议:- 将需要动态添加的值作为


