设计模式:责任链模式(MyBatis)

目录

一、核心角色对应(责任链模式)

[二、MyBatis 责任链的核心拦截点](#二、MyBatis 责任链的核心拦截点)

[三、责任链执行流程(以 StatementHandler 为例)](#三、责任链执行流程(以 StatementHandler 为例))

[1. 整体流程](#1. 整体流程)

[2. 关键环节:嵌套代理构建责任链](#2. 关键环节:嵌套代理构建责任链)

四、代码示例:自定义插件实现责任链

[步骤 1:定义抽象处理者(复用 MyBatis 原生 Interceptor)](#步骤 1:定义抽象处理者(复用 MyBatis 原生 Interceptor))

[步骤 2:实现具体处理者(自定义插件)](#步骤 2:实现具体处理者(自定义插件))

[插件 1:SQL 日志插件(记录最终执行的 SQL)](#插件 1:SQL 日志插件(记录最终执行的 SQL))

[插件 2:分页插件(改写 SQL 拼接分页条件)](#插件 2:分页插件(改写 SQL 拼接分页条件))

[步骤 3:配置插件(构建责任链)](#步骤 3:配置插件(构建责任链))

[步骤 4:执行效果(责任链执行流程)](#步骤 4:执行效果(责任链执行流程))

[五、责任链核心实现:Plugin 类(管理器)](#五、责任链核心实现:Plugin 类(管理器))

关键逻辑:

[六、MyBatis 责任链的核心特点](#六、MyBatis 责任链的核心特点)

[1. 嵌套代理式责任链(区别于列表式)](#1. 嵌套代理式责任链(区别于列表式))

[2. 不可中断的纯责任链](#2. 不可中断的纯责任链)

[3. 精准拦截(按方法签名匹配)](#3. 精准拦截(按方法签名匹配))

[4. 无侵入扩展](#4. 无侵入扩展)

七、典型应用场景

八、使用注意事项

总结


MyBatis 基于责任链模式 + JDK 动态代理 实现了插件(Interceptor)机制,核心目标是对 SQL 执行全流程(参数处理、SQL 构建、结果集返回等)进行无侵入的拦截增强,多个插件可按顺序形成责任链,依次处理目标对象,最终执行原生逻辑。

与 Spring MVC/Security 的 "列表式责任链" 不同,MyBatis 责任链是嵌套代理式:每个插件通过动态代理包裹目标对象,请求沿代理链传递,本质仍是 "请求沿链传递、处理者自主决策" 的责任链核心思想。

一、核心角色对应(责任链模式)

责任链模式角色 MyBatis 对应实现 核心职责
抽象处理者(Handler) org.apache.ibatis.plugin.Interceptor 接口 定义拦截核心方法(intercept)、代理创建方法(plugin),是所有插件的统一接口
具体处理者 自定义插件(如分页插件 PageHelper、SQL 日志插件)、MyBatis 内置插件 实现 Interceptor,指定拦截点(如 StatementHandler 的 prepare 方法),处理拦截逻辑后传递请求
责任链管理器 org.apache.ibatis.plugin.Plugin 类 基于 JDK 动态代理构建插件链,触发拦截方法执行,控制请求传递
被处理的 "请求" 对象 MyBatis 核心执行组件(Executor/StatementHandler/ParameterHandler/ResultSetHandler) 插件拦截的目标,也是请求传递的载体(如修改 StatementHandler 的 SQL 内容)
最终处理者 MyBatis 原生组件的方法(如 StatementHandler.prepare()) 责任链执行完毕后,执行原生 SQL 处理逻辑

二、MyBatis 责任链的核心拦截点

MyBatis 插件可拦截 4 个核心组件的方法(即责任链可处理的 "请求类型"),覆盖 SQL 执行全流程:

拦截组件 拦截时机 典型用途
Executor SQL 执行前 / 后(query/update) 分页、缓存增强、SQL 耗时统计
StatementHandler SQL 构建 / 执行前(prepare/parameterize) SQL 改写、动态拼接条件
ParameterHandler SQL 参数绑定前(setParameters) 参数加密、参数格式化
ResultSetHandler 结果集处理后(handleResultSets) 结果集脱敏、数据类型转换

三、责任链执行流程(以 StatementHandler 为例)

1. 整体流程

plaintext

复制代码
MyBatis 执行 SQL → 
  创建 `StatementHandler` 对象 → 
  Plugin 类为其生成代理对象(嵌套所有插件)→ 
  调用 `StatementHandler.prepare()` → 
  触发代理链执行:
    插件1.intercept() → 处理逻辑(如改写 SQL)→ 调用 `invocation.proceed()` → 
    插件2.intercept() → 处理逻辑(如记录 SQL)→ 调用 `invocation.proceed()` → 
    ... → 
  执行原生 `StatementHandler.prepare()` → 
  执行 SQL 并返回结果

2. 关键环节:嵌套代理构建责任链

MyBatis 插件链通过 Plugin.wrap() 方法嵌套构建,例如配置了 "分页插件 + 日志插件":

plaintext

复制代码
原始 StatementHandler → 
  被日志插件代理(LogPlugin)→ 
  被分页插件代理(PagePlugin)→ 
  最终代理对象

调用代理对象的 prepare() 时,执行顺序为:PagePlugin.intercept() → LogPlugin.intercept() → 原始 prepare(),即配置顺序 = 代理外层顺序 = 拦截执行顺序。

四、代码示例:自定义插件实现责任链

步骤 1:定义抽象处理者(复用 MyBatis 原生 Interceptor)

MyBatis 已提供抽象处理者接口,无需自定义:

java

运行

复制代码
public interface Interceptor {
    // 核心拦截方法:处理逻辑,通过 invocation.proceed() 传递请求
    Object intercept(Invocation invocation) throws Throwable;

    // 创建代理对象:将当前插件加入责任链
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 设置插件属性(从 mybatis-config.xml 读取)
    default void setProperties(Properties properties) {}
}

步骤 2:实现具体处理者(自定义插件)

插件 1:SQL 日志插件(记录最终执行的 SQL)

java

运行

复制代码
@Intercepts({
        // 指定拦截点:拦截 StatementHandler 的 prepare 方法
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class SqlLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取目标对象(StatementHandler)
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. 反射获取 SQL 内容(MyBatis 提供的反射工具)
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");
        // 3. 处理逻辑:打印 SQL
        System.out.println("【SQL 日志插件】执行 SQL:" + sql);
        // 4. 传递请求:执行下一个插件/原生方法
        return invocation.proceed();
    }
}
插件 2:分页插件(改写 SQL 拼接分页条件)

java

运行

复制代码
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class PageInterceptor implements Interceptor {
    private int pageSize; // 每页条数

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取目标对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // 2. 处理逻辑:改写 SQL(拼接 LIMIT)
        String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
        String pageSql = originalSql + " LIMIT " + pageSize;
        metaObject.setValue("delegate.boundSql.sql", pageSql);
        System.out.println("【分页插件】改写 SQL:" + pageSql);
        // 3. 传递请求
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取配置的分页大小
        pageSize = Integer.parseInt(properties.getProperty("pageSize", "10"));
    }
}

步骤 3:配置插件(构建责任链)

在 mybatis-config.xml 中注册插件,配置顺序即责任链执行顺序:

xml

复制代码
<configuration>
    <plugins>
        <!-- 第一个注册:分页插件(外层代理) -->
        <plugin interceptor="com.example.PageInterceptor">
            <property name="pageSize" value="5"/>
        </plugin>
        <!-- 第二个注册:日志插件(内层代理) -->
        <plugin interceptor="com.example.SqlLogInterceptor"/>
    </plugins>
</configuration>

步骤 4:执行效果(责任链执行流程)

当执行 userMapper.selectAll() 时,输出如下:

plaintext

复制代码
【分页插件】改写 SQL:SELECT * FROM user LIMIT 5
【SQL 日志插件】执行 SQL:SELECT * FROM user LIMIT 5
  • 执行顺序:分页插件先拦截(修改 SQL)→ 日志插件后拦截(打印最终 SQL)→ 执行原生 prepare() 方法;
  • 核心传递逻辑:每个插件通过 invocation.proceed() 触发下一个插件 / 原生方法执行,实现 "请求沿链传递"。

五、责任链核心实现:Plugin 类(管理器)

MyBatis 的 Plugin 类是责任链的核心,基于 JDK 动态代理实现拦截和传递,核心源码简化如下:

java

运行

复制代码
public class Plugin implements InvocationHandler {
    private final Object target; // 被代理的目标对象(如 StatementHandler)
    private final Interceptor interceptor; // 当前插件
    private final Map<Class<?>, Set<Method>> signatureMap; // 插件要拦截的方法

    // 创建代理对象(嵌套构建责任链)
    public static Object wrap(Object target, Interceptor interceptor) {
        // 获取插件要拦截的方法映射
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // 获取目标类的所有接口(JDK 动态代理需要接口)
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            // 生成代理对象:当前 Plugin 作为 InvocationHandler
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap)
            );
        }
        return target; // 无需拦截,返回原对象
    }

    // 代理对象调用方法时触发(核心:责任链执行)
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 判断当前方法是否是插件要拦截的方法
        if (signatureMap.containsKey(method.getDeclaringClass())) {
            if (signatureMap.get(method.getDeclaringClass()).contains(method)) {
                // 执行插件的 intercept 方法,传入 Invocation(封装目标、方法、参数)
                return interceptor.intercept(new Invocation(target, method, args));
            }
        }
        // 不拦截,直接执行目标方法(传递请求)
        return method.invoke(target, args);
    }
}

关键逻辑:

  1. wrap() 方法:为目标对象生成代理,若有多个插件,会嵌套生成代理(如 Plugin.wrap(Plugin.wrap(target, pagePlugin), logPlugin));
  2. invoke() 方法:代理对象调用方法时,先执行插件的 intercept 方法,再通过 Invocation.proceed() 触发下一层代理 / 原生方法。

六、MyBatis 责任链的核心特点

1. 嵌套代理式责任链(区别于列表式)

  • 传统责任链(如 Spring MVC):用列表存储处理者,循环执行;
  • MyBatis 责任链:用动态代理嵌套处理者,请求沿代理链传递,更轻量且适配 "拦截方法执行" 的场景。

2. 不可中断的纯责任链

MyBatis 插件必须通过 invocation.proceed() 传递请求(否则原生逻辑无法执行),无法像 Spring MVC 那样 "中断链",属于纯责任链(请求必须传递到最终处理者)。

3. 精准拦截(按方法签名匹配)

通过 @Intercepts + @Signature 注解指定拦截的 "组件 + 方法 + 参数",仅拦截匹配的方法,避免无效处理。

4. 无侵入扩展

新增插件只需实现 Interceptor 并配置,无需修改 MyBatis 源码,符合 "开闭原则"。

七、典型应用场景

  1. 分页插件(PageHelper) :拦截 Executor 的 query 方法或 StatementHandler 的 prepare 方法,拼接分页 SQL(如 LIMIT/ROWNUM);
  2. SQL 监控 / 日志 :拦截 StatementHandler 的 prepare 方法,记录 SQL 内容、执行耗时、参数信息;
  3. 数据脱敏 :拦截 ParameterHandler(入参脱敏,如手机号隐藏中间 4 位)、ResultSetHandler(出参脱敏);
  4. 多租户适配 :拦截 StatementHandler,自动为 SQL 拼接租户 ID 条件(如 WHERE tenant_id = ?);
  5. 参数加密 / 解密 :拦截 ParameterHandler 对敏感参数(如密码)加密,拦截 ResultSetHandler 对结果解密。

八、使用注意事项

  1. 插件顺序:配置顺序决定执行顺序(外层插件先执行),需注意依赖(如分页插件应在日志插件前执行,否则日志打印原 SQL);
  2. 反射安全 :修改 MyBatis 内部属性(如 SQL)时,必须使用 MetaObject(MyBatis 原生反射工具),避免直接反射导致版本兼容问题;
  3. 性能控制 :插件会增加 SQL 执行耗时,避免过多 / 复杂插件(如同时拦截 Executor 和 StatementHandler);
  4. 方法匹配 :@Signature 的 args 必须与目标方法参数完全一致(如 StatementHandler.prepare() 的参数是 Connection 和 Integer,需精准匹配)。

总结

MyBatis 是责任链模式在 "ORM 框架拦截 SQL 执行" 场景的经典落地:

  1. 核心载体 :Interceptor 是抽象处理者,Plugin 是责任链管理器,通过动态代理嵌套构建插件链;
  2. 执行逻辑 :请求沿代理链传递,每个插件处理后通过 invocation.proceed() 传递给下一个插件,最终执行原生逻辑;
  3. 核心价值:无侵入扩展 SQL 执行流程,适配分页、日志、脱敏等多样化业务需求;
  4. 关键扩展 :自定义插件只需实现 Interceptor,配置后即可加入责任链,是 MyBatis 灵活性的核心体现。

掌握这一机制,是开发 MyBatis 定制化插件(如自定义分页、SQL 改写)的核心前提。

相关推荐
代码什么用1 天前
使用javassist生成类 && MyBatis中接口代理机制及使用
mybatis
路弥行至1 天前
【Head First 设计模式】第 2 章:观察者模式 —— 用现代 C++ 重新解读“交互对象的松耦合
c++·经验分享·笔记·观察者模式·设计模式·入门教程·headfirst
YYYing.1 天前
【设计模式系列 (九) 】装饰器模式
c++·后端·设计模式·装饰器模式·c/c++
paopaokaka_luck1 天前
非遗文物数字化小程序(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
javascript·spring boot·mysql·数据分析·echarts·mybatis
韩曙亮1 天前
【JavaEE】MyBatis 基本用法 ⑤ ( 单条件查询 | 多条件查询 | 多条件动态查询 | 单条件动态查询 )
java·sql·java-ee·mybatis·mapper·sql映射
沫璃染墨1 天前
《从零入门Linux系统篇(五十六):线程篇·九——生产者消费者模型:从条件变量到BlockingQueue实现》
linux·服务器·c++·驱动开发·设计模式·架构·系统架构
x-Achan1 天前
Redis实现方案:更新策略、穿透、雪崩、击穿、工具封装
java·spring boot·redis·spring·bootstrap·mybatis
c萱2 天前
AI产品经理——01基础认知篇
设计模式·prompt·aigc·产品经理·ai编程·gitcode·ai-native
YYYing.2 天前
【设计模式系列 (八) 】组合模式
c++·后端·设计模式·组合模式·c/c++
shuoshuohaohao3 天前
MyBatis 小知识点
java·spring·mybatis