MyBatis 的插件体系是如何设计的? 如何编写自定义插件?

插件体系的设计原理:

  1. 基于拦截器 (Interceptor) 模式: 插件体系的核心是 org.apache.ibatis.plugin.Interceptor 接口。我们可以通过实现这个接口来创建自定义插件。
  2. 拦截点: MyBatis 预定义了四个可以被插件拦截的核心接口(及其方法):
    • Executor: SQL 执行器,负责 SQL 的最终执行(增删改查、事务提交回滚、缓存交互等)。可拦截 update, query, commit, rollback 等方法。这是最常用的拦截点之一,比如可以实现分页、分表、读写分离、性能监控等。
    • ParameterHandler: 参数处理器,负责将用户传入的参数对象设置到 JDBC 的 PreparedStatement 中。可拦截 setParameters 方法。可以实现参数加密、特殊类型处理等。
    • ResultSetHandler: 结果集处理器,负责将 JDBC 返回的 ResultSet 映射成 Java 对象。可拦截 handleResultSets, handleOutputParameters 方法。可以实现结果解密、数据脱敏、自定义类型转换等。
    • StatementHandler: 语句处理器,负责处理 JDBC Statement 的操作,包括创建 Statement、设置参数、执行 SQL 等。是 ParameterHandlerResultSetHandler 的上层。可拦截 prepare, parameterize, batch, update, query 等方法。常用于 SQL 重写(如物理分页)、SQL 审计等。
  3. 动态代理 (JDK Dynamic Proxy): MyBatis 插件体系的底层实现依赖于 Java 的动态代理。当 MyBatis 初始化时,如果配置了插件,它会为上述四个核心接口的实例对象创建代理对象
    • 当调用这些核心对象的方法时,实际上是调用了代理对象的方法。
    • 代理对象的处理器 (InvocationHandler) 就是插件自身 (Interceptor 实现类)。
    • 插件的 intercept 方法会被调用,从而获得拦截和修改原始行为的机会。
  4. 责任链模式 (Chain of Responsibility): 如果配置了多个插件,MyBatis 会按照配置的顺序,将这些插件层层包裹(代理)在原始对象外面,形成一个插件链。调用请求会依次经过链上的每个插件的 intercept 方法,最后到达原始对象的方法。

关键接口和注解:

  • org.apache.ibatis.plugin.Interceptor: 插件必须实现的接口。
    • intercept(Invocation invocation): 核心拦截逻辑。Invocation 对象封装了被拦截的目标对象 (target)、目标方法 (Method) 和方法参数 (args)。必须在方法内部调用 invocation.proceed() 来执行原始方法(或者下一个插件的拦截方法),除非你想完全阻止原始方法的执行。
    • plugin(Object target): 用于生成目标对象的代理对象。通常直接使用 MyBatis 提供的工具类 Plugin.wrap(target, this) 来创建代理。这个方法会在 MyBatis 构建核心对象时被调用,判断是否需要为 target 创建代理。
    • setProperties(Properties properties): 用于接收在 MyBatis 配置文件中为该插件配置的属性。
  • org.apache.ibatis.plugin.Invocation: 封装了拦截点信息的对象,传递给 intercept 方法。
  • @Intercepts: 标注在 Interceptor 实现类上,用于声明该插件要拦截哪些方法。它包含一个 @Signature 数组。
  • @Signature: 标注在 @Intercepts 内部,用于定义一个具体的拦截签名。包含三个属性:
    • type: 要拦截的接口类型(Executor.class, ParameterHandler.class 等)。
    • method: 要拦截的方法名(字符串,如 "query", "update")。
    • args: 要拦截的方法的参数类型列表(Class<?>[]),用于精确匹配方法签名。

如何编写自定义插件:

以下是编写自定义 MyBatis 插件的基本步骤:

步骤 1:创建插件类并实现 Interceptor 接口

package com.example.myplugin;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

// 1. 使用 @Intercepts 注解声明拦截点
@Intercepts({
    // 2. 使用 @Signature 定义具体拦截的方法签名
    @Signature(
        type = Executor.class,         // 拦截 Executor 接口
        method = "query",              // 拦截 query 方法
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} // query 方法的参数类型列表
    ),
    // 如果需要拦截多个方法,可以添加更多的 @Signature
    // @Signature(...)
})
public class MyCustomPlugin implements Interceptor {

    private Properties properties = new Properties();

    // 3. 实现 intercept 方法,编写核心拦截逻辑
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyCustomPlugin: Before executing " + invocation.getMethod().getName());

        // 可以获取原始方法调用的信息
        Object target = invocation.getTarget(); // 被拦截的对象 (Executor 实例)
        Object[] args = invocation.getArgs();   // 原始方法的参数
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];

        // === 在执行原始方法前可以做一些事情 ===
        // 例如:修改参数、记录日志、权限校验等
        System.out.println("SQL ID: " + ms.getId());
        System.out.println("Parameter: " + parameter);


        // 4. 调用 invocation.proceed() 执行原始方法 (或责任链中的下一个插件)
        Object result = invocation.proceed();


        // === 在执行原始方法后可以做一些事情 ===
        // 例如:修改结果、记录日志、统计耗时等
        System.out.println("MyCustomPlugin: After executing " + invocation.getMethod().getName());
        System.out.println("Result: " + result); // 注意:result 可能是 List 或 Cursor

        // 5. 返回结果 (可能是修改后的结果)
        return result;
    }

    // 6. 实现 plugin 方法,用于生成代理对象
    @Override
    public Object plugin(Object target) {
        // 使用 MyBatis 提供的 Plugin.wrap 方法为目标对象创建代理
        // 它会检查当前插件是否配置了拦截该 target 类型的方法
        return Plugin.wrap(target, this);
    }

    // 7. 实现 setProperties 方法,接收配置属性 (可选)
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
        System.out.println("MyCustomPlugin Properties: " + properties);
        // 可以在这里读取插件需要的配置,如:
        // String myConfigValue = properties.getProperty("myConfigKey");
    }
}

步骤 2:在 MyBatis 配置文件中注册插件

mybatis-config.xml 文件中,使用 <plugins> 标签来注册你的自定义插件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 其他配置 ... -->

    <plugins>
        <plugin interceptor="com.example.myplugin.MyCustomPlugin">
            <!-- 可以为插件传递属性 -->
            <property name="someProperty" value="someValue"/>
            <property name="anotherProperty" value="anotherValue"/>
        </plugin>
        <!-- 可以配置多个插件,它们会按顺序形成责任链 -->
        <!-- <plugin interceptor="com.example.anotherplugin.AnotherPlugin"/> -->
    </plugins>

    <!-- 其他配置 ... -->

</configuration>

注意事项:

  • @Signatureargs 必须精确匹配: 参数类型和顺序必须与要拦截的方法完全一致,否则插件不会生效。
  • invocation.proceed() 的重要性: 忘记调用 proceed() 会导致原始方法(或后续插件)无法执行。
  • 性能影响: 插件会增加方法调用的开销,尤其是在插件链较长或 intercept 方法逻辑复杂时。需要注意插件对性能的影响。
  • 线程安全: 插件实例通常是单例的,会被多个线程共享。如果插件内部有状态,需要确保线程安全。
  • 参数修改: 如果在 intercept 方法中修改了 invocation.getArgs() 数组中的参数对象,这个修改会传递给原始方法。
  • 结果修改: intercept 方法返回的值会作为最终结果返回给调用者,可以在 proceed() 之后修改结果。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰糖心书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值