面试鸭返利网

mybatis 拦截器 修改 sql

MyBatis拦截器是面试中经常被问到的核心知识点,它能在SQL执行过程中进行拦截和修改,实现分页、数据权限等高级功能。要修改SQL,关键是通过StatementHandler拦截prepare方法,获取BoundSql对象中的原始SQL字符串。通过反射机制可以安全地修改SQL内容,比如添加分页LIMIT或数据权限条件。在实际应用中需要注意SQL注入风险,建议使用预编译参数而非直接拼接。拦截器的实现需要精确控制作用范围,避免影响所有SQL语句。掌握MyBatis拦截器原理不仅能应对面试,更能解决实际开发中的复杂需求,如多租户隔离、逻辑删除等场景,体现了开发者对框架底层机制的深入理解。

Mybatis 拦截器修改 SQL:面试官最爱问的实现原理与实战场景

大家好,我是经常和 Mybatis 打交道的程序员。今天咱们来深入聊聊 Mybatis 里一个非常强大且面试高频的特性——拦截器(Interceptor),特别是如何用它来修改 SQL。面试官特别喜欢问这个,因为它能考察你对 Mybatis 框架执行过程的理解深度和实际应用能力。咱们就以面试中口述回答的思路来拆解它。

H2 Mybatis 拦截器到底是个啥?它能干啥?

说白了,Mybatis 拦截器就像是在 Mybatis 执行 SQL 语句这条流水线上安插的“关卡”。它基于 JDK 动态代理实现,允许你在 Mybatis 核心对象(比如 ExecutorStatementHandlerParameterHandlerResultSetHandler)的方法执行前后“横插一脚”,干点自定义的事情。

它能干的活可多了:

  1. 监控和统计 SQL 执行性能:记录每个 SQL 的执行时间。
  2. 分页功能实现:很多分页插件底层就是靠它拦截 SQL修改(加上 limitoffset 或改写为数据库特定的分页语句)。
  3. 数据权限控制:根据当前用户角色,自动在 SQLWHERE 条件里拼接数据过滤条件。
  4. SQL 改写:比如统一给表名加前缀/后缀、替换特定的 SQL 片段、实现逻辑删除(自动将 deleteupdate ... set deleted=1)等。这就是我们今天要重点讲的“修改 SQL”的核心应用!
  5. 参数和结果的加解密:在设置参数前加密,在获取结果后解密。
  6. 多租户数据隔离:自动在 SQL 中加上租户ID过滤条件。

所以,当你听到面试官问“Mybatis 拦截器怎么用?能做什么?”,或者更具体的“如何用拦截器动态修改 SQL?”,你就知道该聊这个了。

H2 修改 SQL 的关键:拦截 StatementHandler

想在 Mybatis 里修改即将执行的 SQL,拦截哪个接口是关键! 答案是:org.apache.ibatis.executor.statement.StatementHandler。为啥呢?因为它主要负责创建 Statement 对象、预编译带占位符的 SQLprepare 方法)、以及执行 SQL

核心步骤拆解:

  1. 创建你的拦截器类:

    • 实现 org.apache.ibatis.plugin.Interceptor 接口。
    • 核心是重写 intercept(Invocation invocation) 方法,在这里写你的拦截逻辑。
    • 重写 plugin(Object target) 方法,通常用 Plugin.wrap(target, this) 返回代理对象。
    • 重写 setProperties(Properties properties) 方法(可选),用于接收配置的参数。
  2. 指定要拦截的方法:

    • 在你的拦截器类上使用 @Intercepts 注解。
    • 注解里包含一个或多个 @Signature 注解。
    • 为了修改 SQL,我们主要拦截 StatementHandlerprepare(Connection connection, Integer transactionTimeout) 方法! 这个方法的参数 connection 就是数据库连接。这个方法执行后,原始的 SQL 语句(带 #{} 占位符)会被预编译成数据库认识的带 ? 的语句。
    @Intercepts({
        @Signature(type = StatementHandler.class, // 拦截的目标接口
                  method = "prepare", // 拦截的方法名
                  args = {Connection.class, Integer.class}) // 方法的参数类型
    })
    public class MySqlModifyInterceptor implements Interceptor {
        // ... 实现 intercept 等方法
    }
    
  3. 在 intercept 方法里获取并修改 SQL:

    • intercept(Invocation invocation) 方法的参数 Invocation 封装了被拦截的目标对象、方法及其参数。
    • 通过 invocation.getTarget() 获取被拦截的 StatementHandler 对象。
    • 通常我们需要向下转型为 RoutingStatementHandler 或具体的实现类(如 PreparedStatementHandler),然后获取其持有的 BoundSql 对象。BoundSql 是核心!
    • 关键对象:BoundSql
      • BoundSql 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() 继续执行链上的下一个拦截器或原方法。
  4. 注册拦截器:

    • Mybatis 配置文件中(通常是 mybatis-config.xml),在 <plugins> 标签下注册你的拦截器。
    • <plugins>
        <plugin interceptor="com.yourpackage.MySqlModifyInterceptor">
          <!-- 这里可以传递 properties 参数,在 setProperties 方法中接收 -->
          <!-- <property name="someProperty" value="someValue"/> -->
        </plugin>
      </plugins>
      

H2 面试实战:如何回答“你用拦截器修改 SQL 的流程?”

“面试官您好,我使用 Mybatis 拦截器修改 SQL 的核心流程是这样的(结合上面步骤):”

  1. 明确目标: 我要拦截 StatementHandler 接口的 prepare 方法,因为在这个阶段能拿到待执行的原始 SQL (BoundSql)。
  2. 实现拦截器: 定义一个类实现 Interceptor 接口,用 @Intercepts@Signature 注解精确指定拦截点。
  3. 获取并操作 SQL:
    • intercept 方法里,通过 invocation.getTarget() 拿到当前的 StatementHandler
    • 通过 StatementHandler.getBoundSql() 拿到关键的 BoundSql 对象。
    • 调用 boundSql.getSql() 获取原始的 SQL 字符串。
    • 根据业务需求(比如数据权限、分页、逻辑删除、表名替换)对原始的 SQL 字符串进行修改,得到新的 SQL 字符串 (newSql)。
  4. 反射设置新 SQL: 因为 BoundSqlsql 字段没有 setter,所以需要通过反射 (Field.setAccessible(true) + field.set()) 将 newSql 设置回 BoundSql 对象中。
  5. 继续执行: 调用 invocation.proceed() 让流程继续。
  6. 配置生效:Mybatis 的全局配置文件 (mybatis-config.xml) 的 <plugins> 部分注册我的拦截器类。

“举个实际应用场景的例子:比如实现数据行级权限。我拦截了查询 SQL,在 WHERE 条件后面动态拼接上了 AND create_by = #{currentUserId}。这样,用户只能看到自己创建的数据。这里需要注意 SQL 注入问题,拼接时要确保值的安全性,通常会结合参数映射来处理。”

H2 注意事项与避坑指南

  1. 理解执行时机: 修改 SQL 一定要在 Statement 被预编译 (prepare) 之前完成!拦截 prepare 方法是最合适的。
  2. 反射的风险: 修改 BoundSqlsql 字段必须用反射,这依赖于 Mybatis 内部实现细节。虽然 Mybatis 本身对这个字段的使用比较稳定,但跨大版本升级时还是需要测试一下兼容性。面试时提一下这个点,能显示你思考的深度。
  3. 性能考虑: SQL 的字符串操作(尤其是复杂的正则替换或解析)会有性能开销。避免在拦截器里做太重的操作,特别是高频执行的简单 SQL。
  4. 谨慎拦截: 拦截器是全局生效的,要确保你的逻辑只影响需要修改的 SQL。可以通过 BoundSqlSqlCommandType (SELECT, UPDATE, INSERT, DELETE) 或 Mapper Id (boundSql.getSqlCommandType() / MappedStatement.getId()) 来精确判断哪些 SQL 需要被处理。
  5. SQL 注入: 如果你直接在 SQL 字符串里拼接用户输入的值(而不是使用 PreparedStatement 的占位符 ?),极有可能引入 SQL 注入漏洞! 在数据权限等场景动态拼接条件时,强烈建议
    • 将需要动态添加的值作为

如果你想获取更多关于面试鸭的优惠信息,可以访问面试鸭返利网面试鸭优惠网,了解最新的优惠活动和返利政策。

立即加入面试鸭会员 →