Mybatis:从为什么需要插件到具体编写修改SQL的插件实战

说说 MyBatis 的插件运行原理,如何编写一个插件?

如果你用过 MyBatis,可能听说过它的插件机制(也叫拦截器,Interceptor)。通过插件,我们可以在 SQL 执行的某个环节插入自定义逻辑,比如日志记录、性能监控甚至动态修改 SQL。但 MyBatis 的插件是如何运行的?为什么能这么灵活?如何自己动手写一个插件?今天我们从朴素的思路出发,一步步逼近答案,最终搞清楚插件的原理和实现方法。

朴素的第一步:插件是什么?

从最简单的角度看,插件就像一个"中间人",在程序运行时"插一脚",改变或增强原有行为。比如,我们希望在每次 SQL 执行前打印一条日志,朴素的办法可能是直接修改 MyBatis 的源码,在关键位置加上一句 System.out.println。但这显然不现实:源码改动成本高、不优雅,而且 MyBatis 是第三方库,我们通常无法直接修改。于是,我们需要一种更灵活的方式------插件。

问题来了:MyBatis 的核心逻辑(比如 SQL 执行)是封装好的,我们如何在不改源码的情况下介入?让我们带着这个疑问,走进 MyBatis 的插件机制。

初步猜想:拦截特定方法?

既然不能改源码,一个直观的猜想是:MyBatis 提供了某种"钩子"(Hook),让我们在特定环节插入代码。类比 Java 的 Servlet 过滤器或 Spring 的 AOP,我们可能会想到"拦截"这个概念。MyBatis 是否允许我们拦截它的核心方法,比如拦截 SQL 执行的过程?

这个猜想有点靠谱。MyBatis 的插件机制确实叫"拦截器"(Interceptor),名字就暗示了它会拦截某些操作。但具体拦截什么?如何实现?我们需要再深入一步。

走进 MyBatis:插件的核心原理

要理解插件的运行原理,先得知道 MyBatis 的核心组件。MyBatis 的数据库操作主要由以下四个对象驱动:

  1. Executor:负责执行 SQL,包括查询、更新等。
  2. ParameterHandler:处理 SQL 参数的映射和设置。
  3. ResultSetHandler:处理查询结果的映射。
  4. StatementHandler:管理底层的 JDBC Statement。

这四个对象是 MyBatis 的"命脉",几乎所有数据库操作都离不开它们。而 MyBatis 的插件机制,正是通过拦截这四个对象的特定方法来实现的。

动态代理的介入

MyBatis 使用了 JDK 动态代理(类似 Mapper 接口的实现方式),在运行时为这些核心对象生成代理对象。具体流程是:

  • 当 MyBatis 初始化时,会加载配置文件中的插件。
  • 在创建 ExecutorParameterHandler 等对象时,MyBatis 会检查是否有插件需要拦截它们。
  • 如果有插件,MyBatis 会通过 Plugin 类(基于动态代理)生成代理对象,包裹原始对象。
  • 当调用代理对象的方法时,插件的逻辑会被触发。
拦截器的运行时机

插件可以选择拦截上述四个对象的方法。比如:

  • Executorquery 方法:拦截 SQL 查询。
  • StatementHandlerprepare 方法:拦截 SQL 预编译。

这意味着,插件的本质是一个动态代理,在核心对象的方法调用前后插入自定义逻辑。听起来很复杂?让我们通过一个例子逼近真相。

复杂的核心:插件的运行细节

假设我们想写一个插件,在每次 SQL 查询前打印日志。MyBatis 的插件需要实现 Interceptor 接口,包含三个方法:

  • intercept(Invocation invocation):定义拦截逻辑。
  • plugin(Object target):决定是否为目标对象生成代理。
  • setProperties(Properties properties):接收配置参数。
一个简单的插件示例
java 复制代码
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

import java.util.Properties;

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class LogPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("SQL 执行前...");
        Object result = invocation.proceed(); // 调用原始方法
        System.out.println("SQL 执行后...");
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this); // 为目标对象生成代理
    }

    @Override
    public void setProperties(Properties properties) {
        // 可选:接收自定义配置
    }
}
配置插件

mybatis-config.xml 中注册:

xml 复制代码
<plugins>
    <plugin interceptor="com.example.LogPlugin"></plugin>
</plugins>

运行后,每次 Executor.query 被调用时,插件都会打印日志。这验证了我们的猜想:插件通过动态代理拦截核心对象的方法。

运行原理的细节
  1. 初始化阶段
    MyBatis 解析配置文件,加载 LogPlugin 并注册到 InterceptorChain
  2. 对象创建阶段
    创建 Executor 时,InterceptorChain 会遍历所有插件,调用它们的 plugin 方法。如果插件适用(比如目标是 Executor),则生成代理对象。
  3. 方法调用阶段
    调用 executor.query 时,代理对象将请求转发到 intercept 方法,执行自定义逻辑后通过 invocation.proceed() 调用原始方法。

逼近最终方案:如何编写一个插件?

现在我们明白了插件的原理,可以总结出编写插件的完整步骤:

  1. 确定拦截目标
    选择要拦截的对象(ExecutorStatementHandler 等)和方法,查阅 MyBatis 文档明确方法的签名。
  2. 实现 Interceptor 接口
    • intercept 中编写自定义逻辑。
    • plugin 中使用 Plugin.wrap 生成代理。
    • setProperties 中处理配置(可选)。
  3. 添加注解
    @Intercepts@Signature 指定拦截点。
  4. 注册插件
    在配置文件或代码中注册插件。
更复杂的例子:动态修改 SQL

假设我们要编写一个插件,在 SQL 后动态添加 LIMIT 1

java 复制代码
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class LimitPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        String originalSql = boundSql.getSql();
        String newSql = originalSql + " LIMIT 1";
        
        // 通过反射修改 SQL
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, newSql);
        
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

这个插件拦截 StatementHandler.prepare,动态修改 SQL,展示了插件的强大能力。

总结

从朴素的"插件是什么"到复杂的动态代理和反射机制,我们逐步揭示了 MyBatis 插件的运行原理。插件通过拦截核心对象的方法,在不改动源码的情况下实现了高度的扩展性。编写插件时,只需明确拦截点、实现接口并注册,就能轻松定制 MyBatis 的行为。无论是日志记录还是 SQL 修改,MyBatis 的插件机制都为我们提供了无限可能!

相关推荐
跟着珅聪学java20 分钟前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
徐小黑ACG1 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂4 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
杉之5 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch6 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
bobz9656 小时前
k8s 怎么提供虚拟机更好
后端
bobz9657 小时前
nova compute 如何创建 ovs 端口
后端
用键盘当武器的秋刀鱼7 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端
Asthenia04128 小时前
从迷宫到公式:为 NFA 构造正规式
后端
Asthenia04128 小时前
像整理玩具一样:DFA 化简和状态等价性
后端