MyBatis 插件(Interceptor)是 MyBatis 框架留给开发者的一扇"后门"。通过它,你可以在不修改 MyBatis 核心代码的情况下,干预或增强 MyBatis 的执行过程。
它的设计非常巧妙,接下来我从"它能做什么"、"它的核心原理"和"一个生动的比喻"三个方面,帮你彻底搞懂它。
🚀 它能做什么?(典型应用场景)
MyBatis 插件在真实项目中应用广泛,常见的有:
| 应用场景 | 具体实现 |
|---|---|
| SQL 日志打印 | 拦截 SQL 执行,打印完整的 SQL 语句、参数和耗时,方便开发和调试。 |
| 数据权限控制 | 在 SQL 执行前自动拼接 WHERE 条件(如 WHERE tenant_id = ?),实现多租户数据隔离。 |
| 分页查询 | 拦截 Executor 的 query 方法,自动改写 SQL,在末尾拼接 LIMIT 和 OFFSET,这是 PageHelper 等分页插件的实现原理。 |
| SQL 性能监控 | 统计 SQL 的执行时间,当超过设定的慢查询阈值时,自动记录日志或发送告警。 |
| 加密/解密字段 | 在 SQL 执行前对敏感字段(如手机号、身份证)进行加密,执行后对结果集进行解密。 |
| 动态替换表名 | 在 SQL 执行前替换表名,常用于分表场景(如将 order 替换为 order_202607)。 |
🔧 它的核心原理(基于"责任链 + 动态代理")
MyBatis 插件的底层原理是 JDK 动态代理。它的执行逻辑可以概括为四个步骤:
-
允许拦截的核心对象 :MyBatis 只允许对 4 大核心接口 的方法进行拦截:
Executor:执行器,负责 SQL 的整体执行。StatementHandler:处理器,负责 JDBC 语句的创建和参数设置。ParameterHandler:参数处理器,负责将参数设置到 SQL 中。ResultSetHandler:结果集处理器,负责将查询结果映射成 Java 对象。
-
创建代理对象 :在 MyBatis 启动时,会解析你配置的插件。当创建上述 4 大核心对象时,MyBatis 会检查是否有插件需要拦截该对象。如果有,就会通过 JDK 动态代理 生成一个代理对象,并将原对象作为目标对象传入。
-
形成责任链 :如果多个插件都拦截了同一个对象,MyBatis 会层层嵌套地创建代理,形成一个责任链。调用时,会从最外层代理开始,依次向内执行。
-
执行拦截逻辑 :当你调用代理对象的方法时(如执行 SQL 的
query方法),会先执行插件中intercept方法里的代码。在这里,你可以:- 获取或修改传入的 SQL、参数。
- 执行
invocation.proceed(),继续执行原方法(或后续插件)。 - 修改或包装返回的结果。
🏠 一个生动的比喻:小区的"安防系统"
假设你住在一个有严格安防的小区(MyBatis),外卖员(用户请求)想通过大门(4 大核心对象)进入。
- 小区门口有一个保安亭(插件)。
- 保安(
Interceptor)会检查外卖员的证件(SQL 语句) 和物品(参数)。 - 保安可以根据规定放行(执行 SQL),或拒绝(抛出异常),甚至可以对物品进行登记(记录日志) 或替换(改写 SQL)。
这个"保安"就是 MyBatis 插件,它在大门(核心方法)入口处,对进出的所有活动进行控制。
🛠️ 简单使用步骤
- 创建类并实现
Interceptor接口。 - 使用
@Intercepts注解,指定要拦截的目标接口和方法。 - 在
intercept方法中 ,通过Invocation参数获取和修改 SQL、参数等信息,然后调用invocation.proceed()继续执行。 - 配置插件 :在 MyBatis 配置文件 (
mybatis-config.xml) 中,通过<plugin>标签注册插件,或在 Spring Boot 中配置为 Bean。
⚠️ 注意事项
- 不要过度使用:每个插件都会增加一层代理,过多插件会影响性能。
- 方法签名敏感 :
@Intercepts中指定的方法签名必须精准匹配,否则插件不会生效。 - 注意分页插件的顺序 :如果一个请求中同时有分页插件和数据权限插件,它们的执行顺序(在
mybatis-config.xml中的配置顺序)会影响最终生成的 SQL。 - 内部对象复杂 :操作
StatementHandler或ResultSetHandler时,涉及较多的内部对象,对 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 使用
StatementHandler 是 Executor 内部使用的,我们不会直接调用它。但它负责的工作(创建 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() |
结果加密/解密、字段脱敏、结果转换 |
四、重要提示
- 不要在插件中频繁创建对象 :插件会拦截每次 SQL 执行,性能敏感,避免在
intercept方法中做重操作。 - 记得调用
invocation.proceed():否则 MyBatis 不会继续执行,SQL 不会真正执行。 - 注意拦截顺序:多个插件会按配置顺序形成责任链,顺序会影响最终结果(如分页插件通常在 SQL 改写插件之前)。
- 反射有风险 :使用
MetaObject或原生反射时,注意字段名可能在不同 MyBatis 版本中有变化。