mybatis插件

MyBatis 插件(Interceptor)是 MyBatis 框架留给开发者的一扇"后门"。通过它,你可以在不修改 MyBatis 核心代码的情况下,干预或增强 MyBatis 的执行过程

它的设计非常巧妙,接下来我从"它能做什么"、"它的核心原理"和"一个生动的比喻"三个方面,帮你彻底搞懂它。


🚀 它能做什么?(典型应用场景)

MyBatis 插件在真实项目中应用广泛,常见的有:

应用场景 具体实现
SQL 日志打印 拦截 SQL 执行,打印完整的 SQL 语句、参数和耗时,方便开发和调试。
数据权限控制 在 SQL 执行前自动拼接 WHERE 条件(如 WHERE tenant_id = ?),实现多租户数据隔离。
分页查询 拦截 Executorquery 方法,自动改写 SQL,在末尾拼接 LIMITOFFSET,这是 PageHelper 等分页插件的实现原理。
SQL 性能监控 统计 SQL 的执行时间,当超过设定的慢查询阈值时,自动记录日志或发送告警。
加密/解密字段 在 SQL 执行前对敏感字段(如手机号、身份证)进行加密,执行后对结果集进行解密。
动态替换表名 在 SQL 执行前替换表名,常用于分表场景(如将 order 替换为 order_202607)。

🔧 它的核心原理(基于"责任链 + 动态代理")

MyBatis 插件的底层原理是 JDK 动态代理。它的执行逻辑可以概括为四个步骤:

  1. 允许拦截的核心对象 :MyBatis 只允许对 4 大核心接口 的方法进行拦截:

    • Executor:执行器,负责 SQL 的整体执行。
    • StatementHandler:处理器,负责 JDBC 语句的创建和参数设置。
    • ParameterHandler:参数处理器,负责将参数设置到 SQL 中。
    • ResultSetHandler:结果集处理器,负责将查询结果映射成 Java 对象。
  2. 创建代理对象 :在 MyBatis 启动时,会解析你配置的插件。当创建上述 4 大核心对象时,MyBatis 会检查是否有插件需要拦截该对象。如果有,就会通过 JDK 动态代理 生成一个代理对象,并将原对象作为目标对象传入。

  3. 形成责任链 :如果多个插件都拦截了同一个对象,MyBatis 会层层嵌套地创建代理,形成一个责任链。调用时,会从最外层代理开始,依次向内执行。

  4. 执行拦截逻辑 :当你调用代理对象的方法时(如执行 SQL 的 query 方法),会先执行插件中 intercept 方法里的代码。在这里,你可以:

    • 获取或修改传入的 SQL、参数。
    • 执行 invocation.proceed(),继续执行原方法(或后续插件)。
    • 修改或包装返回的结果。

🏠 一个生动的比喻:小区的"安防系统"

假设你住在一个有严格安防的小区(MyBatis),外卖员(用户请求)想通过大门(4 大核心对象)进入。

  • 小区门口有一个保安亭(插件)
  • 保安(Interceptor)会检查外卖员的证件(SQL 语句)物品(参数)
  • 保安可以根据规定放行(执行 SQL),或拒绝(抛出异常),甚至可以对物品进行登记(记录日志)替换(改写 SQL)

这个"保安"就是 MyBatis 插件,它在大门(核心方法)入口处,对进出的所有活动进行控制。


🛠️ 简单使用步骤

  1. 创建类并实现 Interceptor 接口
  2. 使用 @Intercepts 注解,指定要拦截的目标接口和方法。
  3. intercept 方法中 ,通过 Invocation 参数获取和修改 SQL、参数等信息,然后调用 invocation.proceed() 继续执行。
  4. 配置插件 :在 MyBatis 配置文件 (mybatis-config.xml) 中,通过 <plugin> 标签注册插件,或在 Spring Boot 中配置为 Bean。

⚠️ 注意事项

  • 不要过度使用:每个插件都会增加一层代理,过多插件会影响性能。
  • 方法签名敏感@Intercepts 中指定的方法签名必须精准匹配,否则插件不会生效。
  • 注意分页插件的顺序 :如果一个请求中同时有分页插件和数据权限插件,它们的执行顺序(在 mybatis-config.xml 中的配置顺序)会影响最终生成的 SQL。
  • 内部对象复杂 :操作 StatementHandlerResultSetHandler 时,涉及较多的内部对象,对 MyBatis 源码的熟悉程度要求较高。

如果想了解如何编写一个简单的 SQL 日志插件,或者想看看 PageHelper 分页插件的核心源码是怎么实现的,我们可以继续深入。

这四个核心组件是 MyBatis 的内部机制,通常我们不会直接实例化或调用它们 ,而是通过 MyBatis 的 API 间接使用。不过,理解它们的使用方式,对于编写插件、调试源码、深入理解 MyBatis 非常有帮助。

我分两部分来讲:正常使用方式插件开发中的使用方式


一、正常使用方式(通过 MyBatis API 间接调用)

作为普通开发者,我们不会直接 new Executor() 或调用 StatementHandler,而是通过 MyBatis 提供的 API 来使用它们。

1. Executor ------ 通过 SqlSession 使用

复制代码

// 方式1:通过 SqlSession 直接调用(最常用) try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.selectById(1); // 内部会调用 Executor } // 方式2:SqlSession 的 selectOne/selectList 方法 try (SqlSession sqlSession = sqlSessionFactory.openSession()) { User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectById", 1); }

2. StatementHandler ------ 通过 SqlSession 使用

StatementHandlerExecutor 内部使用的,我们不会直接调用它。但它负责的工作(创建 PreparedStatement、设置参数、执行 SQL)最终会通过 JDBC 完成。

3. ParameterHandler ------ 通过 SqlSession 使用

我们通过 Mapper 接口传入参数,MyBatis 内部会使用 ParameterHandler 来处理这些参数,但对我们来说是透明的。

4. ResultSetHandler ------ 通过 SqlSession 使用

当我们调用 Mapper 方法时,返回的 Java 对象就是 ResultSetHandler 处理完 ResultSet 后映射的结果。


二、插件开发中的使用方式(进阶)

这是最常用的场景。在自定义 MyBatis 插件(Interceptor)中,我们可以直接操作这四个组件的核心方法。

1. Executor 的使用:拦截查询,实现 SQL 审计

复制代码

@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SqlAuditInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取参数 MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; // 2. 获取 SQL 信息 BoundSql boundSql = ms.getBoundSql(parameter); String sql = boundSql.getSql(); // 3. 记录日志(审计) System.out.println("执行的 SQL:" + sql); System.out.println("参数:" + parameter); long startTime = System.currentTimeMillis(); // 4. 继续执行原方法(调用下一个拦截器或真正的查询) Object result = invocation.proceed(); long endTime = System.currentTimeMillis(); System.out.println("执行耗时:" + (endTime - startTime) + "ms"); return result; } }

关键点

  • 通过 invocation.getArgs() 获取 MappedStatement 和参数。
  • 通过 BoundSql 获取完整的 SQL。
  • 调用 invocation.proceed() 让 MyBatis 继续执行。

2. StatementHandler 的使用:改写 SQL(如分页插件)

复制代码

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class PageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取 StatementHandler StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 2. 通过反射获取 BoundSql MetaObject metaObject = SystemMetaObject.forObject(statementHandler); BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); // 3. 获取原始 SQL String originalSql = boundSql.getSql(); // 4. 判断是否需要分页(通过 ThreadLocal 传递分页参数) PageParam pageParam = PageContext.get(); if (pageParam != null) { // 5. 改写 SQL,拼接 LIMIT String newSql = originalSql + " LIMIT " + pageParam.getOffset() + ", " + pageParam.getPageSize(); // 6. 通过反射修改 BoundSql 中的 SQL Field field = BoundSql.class.getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, newSql); } // 7. 继续执行 return invocation.proceed(); } }

关键点

  • 通过 MetaObject 反射获取 BoundSql
  • 修改 BoundSql 中的 SQL 语句。
  • 利用 ThreadLocal 传递分页参数。

3. ParameterHandler 的使用:修改参数值

复制代码

@Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}) }) public class ParameterEncryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取 ParameterHandler 和 PreparedStatement ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget(); PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; // 2. 通过反射获取参数对象 MetaObject metaObject = SystemMetaObject.forObject(parameterHandler); Object parameterObject = metaObject.getValue("parameterObject"); // 3. 如果参数是手机号,进行加密 if (parameterObject instanceof User) { User user = (User) parameterObject; String phone = user.getPhone(); if (phone != null && phone.length() == 11) { // 加密手机号(示例:替换中间4位) user.setPhone(phone.substring(0, 3) + "****" + phone.substring(7)); } } // 4. 继续执行(这里会调用 JDBC 的 setXxx 方法) return invocation.proceed(); } }

关键点

  • 通过反射获取 parameterObject(传入的参数对象)。
  • 修改参数值后,继续执行 setParameters,MyBatis 会用修改后的值设置到 PreparedStatement

4. ResultSetHandler 的使用:修改返回结果

复制代码

@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class ResultDecryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 继续执行原方法,获取结果 Object result = invocation.proceed(); // 2. 如果结果是 List,遍历解密 if (result instanceof List) { List<?> list = (List<?>) result; for (Object obj : list) { if (obj instanceof User) { User user = (User) obj; String phone = user.getPhone(); if (phone != null && phone.contains("****")) { // 解密(示例:这里简单还原,实际需要真正的解密算法) user.setPhone(phone.replace("****", "1234")); } } } } return result; } }

关键点

  • 先调用 invocation.proceed() 获取结果,再修改结果。
  • 在返回之前,对结果集中的敏感字段进行解密。

三、四种组件的插件使用场景总结

组件 拦截方法 典型应用场景
Executor query(), update() SQL 审计、慢查询日志、全局限流
StatementHandler prepare(), parameterize(), query() 分页插件(PageHelper)、SQL 改写、表名替换
ParameterHandler setParameters() 参数加密、参数校验、参数转换
ResultSetHandler handleResultSets() 结果加密/解密、字段脱敏、结果转换

四、重要提示

  1. 不要在插件中频繁创建对象 :插件会拦截每次 SQL 执行,性能敏感,避免在 intercept 方法中做重操作。
  2. 记得调用 invocation.proceed():否则 MyBatis 不会继续执行,SQL 不会真正执行。
  3. 注意拦截顺序:多个插件会按配置顺序形成责任链,顺序会影响最终结果(如分页插件通常在 SQL 改写插件之前)。
  4. 反射有风险 :使用 MetaObject 或原生反射时,注意字段名可能在不同 MyBatis 版本中有变化。
相关推荐
橘子海全栈攻城狮7 小时前
【最新源码】基于SpringBoot + Vue的超市管理系统的设计与实现D002
java·开发语言·vue.js·spring boot·后端·spring
smartvxworks7 小时前
Linux 实时内核(Linux Real-Time Kernel)详解:原理、实践与优化
java·linux·服务器
qizayaoshuap7 小时前
# [特殊字符] 密码生成器 — 鸿蒙ArkTS安全算法与密码强度评估系统
java·算法·安全·华为·harmonyos
AC赳赳老秦7 小时前
OpenClaw 采集任务日志审计:全程记录采集行为,满足合规溯源与企业审计要求
java·大数据·python·数据挖掘·数据分析·php·openclaw
风起洛阳@不良使8 小时前
spring中xml和注解开发的对比
xml·java·spring
Tenifs8 小时前
关闭 IDEA 的英文单词拼写检查
java·intellij-idea
captain3768 小时前
创建多线程程序
java·java-ee
野蛮人6号8 小时前
黑马天机学堂Day01-08.搭建项目环境-本地开发部署方式——不知道nacos的密码是什么
java·spring cloud·黑马程序员·天机学堂·黑马天机学堂
Roy_Sashulin8 小时前
灵杉Java编程平台第一章:安装
java·开发语言