设计模式:责任链模式(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,指定拦截点(如 StatementHandlerprepare 方法),处理拦截逻辑后传递请求
责任链管理器 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) :拦截 Executorquery 方法或 StatementHandlerprepare 方法,拼接分页 SQL(如 LIMIT/ROWNUM);
  2. SQL 监控 / 日志 :拦截 StatementHandlerprepare 方法,记录 SQL 内容、执行耗时、参数信息;
  3. 数据脱敏 :拦截 ParameterHandler(入参脱敏,如手机号隐藏中间 4 位)、ResultSetHandler(出参脱敏);
  4. 多租户适配 :拦截 StatementHandler,自动为 SQL 拼接租户 ID 条件(如 WHERE tenant_id = ?);
  5. 参数加密 / 解密 :拦截 ParameterHandler 对敏感参数(如密码)加密,拦截 ResultSetHandler 对结果解密。

八、使用注意事项

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

总结

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

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

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

相关推荐
无名-CODING2 小时前
MyBatis 动态 SQL 全攻略
数据库·sql·mybatis
崎岖Qiu4 小时前
【设计模式笔记19】:建造者模式
java·笔记·设计模式·建造者模式
syt_10134 小时前
设计模式之-享元模式
javascript·设计模式·享元模式
java1234_小锋14 小时前
[免费]SpringBoot+Vue勤工助学管理系统【论文+源码+SQL脚本】
spring boot·后端·mybatis·勤工助学
一灰灰16 小时前
开发调试与生产分析的利器:MyBatis SQL日志合并插件,让复杂日志秒变可执行SQL
chrome·后端·mybatis
故渊ZY16 小时前
MyBatis事务原理与实战指南
java·mybatis
想学后端的前端工程师18 小时前
【Java设计模式实战应用指南:23种设计模式详解】
java·开发语言·设计模式
Revol_C18 小时前
开箱即用!轻量级轮询方案,支持同步获取轮询结果!
前端·javascript·设计模式
长安er18 小时前
LeetCode 62/64/5/1143多维动态规划核心题型总结
算法·leetcode·mybatis·动态规划