news 2026/7/22 2:23:12

mybatis插件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 版本中有变化。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 2:23:00

开源生产力工具全指南:从Linux到Blender

1. 开源世界的宝藏地图在数字时代&#xff0c;自由软件早已不再是极客们的专属玩具。从操作系统到生产力工具&#xff0c;从创意设计到开发环境&#xff0c;开源社区孕育了无数媲美商业软件的优秀产品。作为一名在开源领域摸爬滚打十年的老手&#xff0c;我亲历了GIMP替代Photo…

作者头像 李华
网站建设 2026/7/22 2:23:01

.NET开发者必看:MAF与LangChain的AI框架选型指南

1. 技术选择的十字路口 2025年的深夜&#xff0c;当最后一行代码提交后&#xff0c;技术总监的消息让每个.NET开发者都陷入了沉思&#xff1a;AI智能体项目该选择LangChain还是MAF&#xff1f;这不只是一个简单的技术选型&#xff0c;而是关乎职业发展路径的战略决策。 作为深…

作者头像 李华
网站建设 2026/7/22 2:23:06

深入解析Ruby on Rails框架:核心组件与开发实践

1. Rails框架概述与核心价值 Ruby on Rails&#xff08;简称Rails&#xff09;是一个开源的Web应用框架&#xff0c;采用MVC架构模式&#xff0c;基于Ruby语言开发。作为一个"全栈框架"&#xff0c;它提供了从数据库操作到前端渲染的完整解决方案。Rails最显著的特点…

作者头像 李华
网站建设 2026/7/22 4:35:09

树莓派安装Ubuntu系统指南与优化技巧

1. 为什么选择Ubuntu作为树莓派操作系统树莓派作为一款广受欢迎的微型计算机&#xff0c;其官方推荐的操作系统是Raspbian&#xff08;现更名为Raspberry Pi OS&#xff09;。但Ubuntu作为全球最流行的Linux发行版之一&#xff0c;在树莓派上也有其独特的优势。首先&#xff0c…

作者头像 李华
网站建设 2026/7/22 4:36:01

Dify与Coze:AI工作流平台架构与选型深度对比

1. 项目概述&#xff1a;AI工作流平台选型之争 在AI技术平民化的浪潮中&#xff0c;低代码/无代码平台正成为企业智能化转型的加速器。作为2023年最受关注的两大AI工作流平台&#xff0c;Dify和Coze各自以独特的技术路径争夺着"智能自动化中枢"的宝座。Dify凭借其开源…

作者头像 李华
网站建设 2026/7/20 21:52:51

Java学习路线与书籍推荐:从入门到进阶

1. Java学习路线与书籍选择逻辑 对于Java开发者来说&#xff0c;书籍选择需要遵循"金字塔式"学习路径。我根据多年教学和项目经验&#xff0c;将Java知识体系划分为四个关键层级&#xff1a; 基础层&#xff08;30%精力&#xff09;&#xff1a;语法基础、面向对象、…

作者头像 李华