当前位置: 首页 > news >正文

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()获取结果,再修改结果。
  • 在返回之前,对结果集中的敏感字段进行解密。

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

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

四、重要提示

  1. 不要在插件中频繁创建对象:插件会拦截每次 SQL 执行,性能敏感,避免在intercept方法中做重操作。
  2. 记得调用invocation.proceed():否则 MyBatis 不会继续执行,SQL 不会真正执行。
  3. 注意拦截顺序:多个插件会按配置顺序形成责任链,顺序会影响最终结果(如分页插件通常在 SQL 改写插件之前)。
  4. 反射有风险:使用MetaObject或原生反射时,注意字段名可能在不同 MyBatis 版本中有变化。
http://www.cnnetsun.cn/news/3545566.html

相关文章:

  • 开源生产力工具全指南:从Linux到Blender
  • .NET开发者必看:MAF与LangChain的AI框架选型指南
  • 深入解析Ruby on Rails框架:核心组件与开发实践
  • 树莓派安装Ubuntu系统指南与优化技巧
  • Dify与Coze:AI工作流平台架构与选型深度对比
  • Java学习路线与书籍推荐:从入门到进阶
  • Java程序员必备英语技能与高效学习路径
  • 付费OEM贴牌包含哪些增值服务
  • 【单片机毕业设计推荐】基于 STM32 的智能定时取药提醒装置设计与实现,基于 STM32 单片机的红外感应药盒控制系统开发(024302)
  • DynaMiCS:大语言模型动态混合微调框架的原理与实践
  • Linux 零拷贝的代价:sendfile/splice/MSG_ZEROCOPY 在小包场景的性能拐点与决策
  • 【AI设计生产力临界点】:掌握这3类动态反馈闭环,让产出质量与速度同步飙升
  • 深入AM275x GPIO中断与I2C协议:寄存器级配置与实战调试指南
  • 从“药学人窒息13天”解析学习状态Vlog的创作逻辑与情绪共鸣
  • 飞书机器人替代短信验证码的完整实现方案
  • 龙芯LoongArch架构解析与开发实践
  • 【Seed Audio 1.0技术解析】字节跳动统一建模人声与音效的端到端影视级音频生成
  • 股票价格预测的正确姿势:从收益率建模到实盘回测
  • 2026时辰不确定时怎么用排盘工具做对照:保留多个假设,比强行定一个答案更稳妥
  • 2026大模型AI技术全景与学习路线解析
  • WVP-GB28181-Pro:一站式企业级国标视频监控解决方案
  • AM275x引脚配置深度解析:PADCFG_CTRL寄存器实战指南
  • AM64x/AM243x MCU_PLL0时钟配置:从寄存器详解到实战避坑指南
  • UG NX CAM编程实战:从零到一生成G代码的完整流程与核心技巧
  • 谷歌SEO运营到底忙什么?从查数据到改代码,一份给纯小白的“无废话”工作实录
  • 桌面Agent记忆管理翻车实录:我的跨会话偏好为何变成「脏数据」?
  • 2026年外贸产品页怎么写?参数、应用场景和询盘转化指南
  • LangGraph框架:构建持久化智能体的底层引擎
  • DDR内存控制器时序与DFI接口深度调优:从原理到AM64x实战
  • libgit2 v1.9.6 发布:修复 Android 系统 segfault 等重要错误