设计模式:责任链模式(mybatis数据权限实现)

目录

[一、先理清核心:MyBatis 责任链 + 数据权限插件的结合逻辑](#一、先理清核心:MyBatis 责任链 + 数据权限插件的结合逻辑)

二、数据权限插件的完整实现(基于责任链)

[步骤 1:定义数据权限上下文(存储当前用户的权限信息)](#步骤 1:定义数据权限上下文(存储当前用户的权限信息))

[步骤 2:实现数据权限插件(责任链的具体处理者)](#步骤 2:实现数据权限插件(责任链的具体处理者))

[步骤 3:配置插件(将数据权限插件加入责任链)](#步骤 3:配置插件(将数据权限插件加入责任链))

[步骤 4:业务中设置数据权限上下文(拦截前注入权限)](#步骤 4:业务中设置数据权限上下文(拦截前注入权限))

[步骤 5:业务 Mapper(无需修改,插件自动过滤)](#步骤 5:业务 Mapper(无需修改,插件自动过滤))

三、责任链执行流程(数据权限插件)

[1. 完整链路](#1. 完整链路)

[2. 执行效果示例](#2. 执行效果示例)

[四、MyBatis 责任链在数据权限插件中的核心细节](#四、MyBatis 责任链在数据权限插件中的核心细节)

[1. 责任链的 "传递性"](#1. 责任链的 “传递性”)

[2. 插件顺序的影响](#2. 插件顺序的影响)

[3. MetaObject 的核心作用](#3. MetaObject 的核心作用)

[4. 多租户场景的扩展(责任链的灵活性)](#4. 多租户场景的扩展(责任链的灵活性))

五、数据权限插件的进阶优化(责任链扩展)

[1. 支持注解指定过滤规则](#1. 支持注解指定过滤规则)

[2. 避免 SQL 改写错误(兼容复杂 SQL)](#2. 避免 SQL 改写错误(兼容复杂 SQL))

六、总结:责任链模式在数据权限插件中的价值


一、先理清核心:MyBatis 责任链 + 数据权限插件的结合逻辑

数据权限是业务中典型的 "动态过滤数据" 场景(如按租户、部门、角色过滤 SQL 结果),MyBatis 基于责任链模式 的插件机制,可在 SQL 执行前动态拼接数据权限条件(如 WHERE dept_id = ?),且不侵入业务 Mapper 代码。

核心思路:

  1. 数据权限插件作为责任链的具体处理者 ,实现 Interceptor 接口;
  2. 拦截 StatementHandlerprepare 方法(SQL 构建阶段);
  3. 解析原 SQL,根据当前用户的权限动态拼接数据权限条件;
  4. 通过 invocation.proceed() 传递请求到下一个插件 / 原生逻辑,完成 SQL 执行。

二、数据权限插件的完整实现(基于责任链)

步骤 1:定义数据权限上下文(存储当前用户的权限信息)

用于在插件中获取当前用户的权限范围(如部门 ID、租户 ID):

java

运行

复制代码
/**
 * 数据权限上下文(ThreadLocal 存储,保证线程安全)
 */
public class DataPermissionContext {
    private static final ThreadLocal<DataPermission> CONTEXT = new ThreadLocal<>();

    // 设置当前用户的数据权限
    public static void set(DataPermission dataPermission) {
        CONTEXT.set(dataPermission);
    }

    // 获取当前用户的数据权限
    public static DataPermission get() {
        return CONTEXT.get();
    }

    // 清理上下文(避免内存泄漏)
    public static void clear() {
        CONTEXT.remove();
    }

    /**
     * 数据权限实体
     */
    @Data
    public static class DataPermission {
        private Long userId; // 当前用户ID
        private Long deptId; // 当前用户部门ID
        private Long tenantId; // 租户ID(多租户场景)
        private boolean isAdmin; // 是否超级管理员(无需过滤)
    }
}

步骤 2:实现数据权限插件(责任链的具体处理者)

核心是拦截 StatementHandlerprepare 方法,改写 SQL 拼接数据权限条件:

java

运行

复制代码
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Properties;

/**
 * 数据权限插件(责任链的具体处理者)
 * 拦截 StatementHandler.prepare(),动态拼接数据权限条件
 */
@Intercepts({
        @Signature(
                type = StatementHandler.class, // 拦截的核心组件
                method = "prepare", // 拦截的方法(SQL 预处理阶段)
                args = {Connection.class, Integer.class} // 方法参数(匹配重载)
        )
})
public class DataPermissionInterceptor implements Interceptor {
    // 需过滤的表名(可通过配置注入)
    private static final String[] FILTER_TABLES = {"sys_user", "sys_order", "sys_leave"};

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取当前用户的数据权限上下文
        DataPermissionContext.DataPermission permission = DataPermissionContext.get();
        // 超级管理员/无权限上下文 → 直接放行,传递请求
        if (permission == null || permission.isAdmin()) {
            return invocation.proceed();
        }

        // 2. 获取被拦截的 StatementHandler 对象(责任链的处理目标)
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // 3. 解析原 SQL 和 MappedStatement(获取 SQL 相关信息)
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        String originalSql = boundSql.getSql().trim();
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String sqlId = mappedStatement.getId(); // 如:com.example.mapper.UserMapper.selectList

        // 4. 判断是否需要拼接数据权限条件(仅过滤指定表)
        boolean needFilter = needFilterTable(originalSql);
        if (!needFilter) {
            return invocation.proceed(); // 无需过滤,传递请求
        }

        // 5. 动态拼接数据权限条件(核心:改写 SQL)
        String newSql = buildDataPermissionSql(originalSql, permission);
        // 替换 BoundSql 中的 SQL(通过 MetaObject 保证反射安全)
        metaObject.setValue("delegate.boundSql.sql", newSql);

        System.out.println("【数据权限插件】原 SQL:" + originalSql);
        System.out.println("【数据权限插件】改写后 SQL:" + newSql);

        // 6. 传递请求到下一个插件/原生逻辑(责任链核心:不可中断,必须 proceed)
        return invocation.proceed();
    }

    /**
     * 判断 SQL 是否包含需要过滤的表
     */
    private boolean needFilterTable(String sql) {
        for (String table : FILTER_TABLES) {
            if (sql.toLowerCase().contains(table.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 拼接数据权限条件(按部门过滤)
     */
    private String buildDataPermissionSql(String originalSql, DataPermissionContext.DataPermission permission) {
        // 处理 SELECT 语句(兼容 WHERE/AND/OR 场景)
        if (originalSql.toUpperCase().startsWith("SELECT")) {
            int whereIndex = originalSql.toUpperCase().indexOf("WHERE");
            if (whereIndex > 0) {
                // 已有 WHERE 条件,拼接 AND dept_id = ?
                return originalSql.substring(0, whereIndex + 5)
                        + " dept_id = " + permission.getDeptId() + " AND "
                        + originalSql.substring(whereIndex + 5);
            } else {
                // 无 WHERE 条件,新增 WHERE dept_id = ?
                return originalSql + " WHERE dept_id = " + permission.getDeptId();
            }
        }
        // 处理 UPDATE/DELETE 语句(同理)
        else if (originalSql.toUpperCase().startsWith("UPDATE") || originalSql.toUpperCase().startsWith("DELETE")) {
            int whereIndex = originalSql.toUpperCase().indexOf("WHERE");
            if (whereIndex > 0) {
                return originalSql.substring(0, whereIndex + 5)
                        + " dept_id = " + permission.getDeptId() + " AND "
                        + originalSql.substring(whereIndex + 5);
            } else {
                return originalSql + " WHERE dept_id = " + permission.getDeptId();
            }
        }
        return originalSql;
    }

    /**
     * 生成代理对象(将插件加入责任链)
     */
    @Override
    public Object plugin(Object target) {
        // Plugin.wrap:MyBatis 责任链管理器,嵌套构建代理链
        return Plugin.wrap(target, this);
    }

    /**
     * 加载插件配置(如过滤表名可从配置文件读取)
     */
    @Override
    public void setProperties(Properties properties) {
        // 示例:从配置读取需过滤的表名
        String tables = properties.getProperty("filterTables");
        if (tables != null && !tables.isEmpty()) {
            FILTER_TABLES = tables.split(",");
        }
    }
}

步骤 3:配置插件(将数据权限插件加入责任链)

mybatis-config.xml 中注册插件,指定执行顺序(若有其他插件,需注意顺序):

xml

复制代码
<configuration>
    <plugins>
        <!-- 数据权限插件(责任链的具体处理者) -->
        <plugin interceptor="com.example.plugin.DataPermissionInterceptor">
            <!-- 配置需过滤的表名(可选) -->
            <property name="filterTables" value="sys_user,sys_order,sys_leave"/>
        </plugin>
        <!-- 其他插件(如日志插件、分页插件)→ 按顺序加入责任链 -->
        <plugin interceptor="com.example.plugin.SqlLogInterceptor"/>
    </plugins>
</configuration>

步骤 4:业务中设置数据权限上下文(拦截前注入权限)

在请求入口(如 Spring MVC 拦截器 / 过滤器)设置当前用户的数据权限:

java

运行

复制代码
/**
 * Spring MVC 拦截器:请求到达 Controller 前,设置数据权限上下文
 */
@Component
public class DataPermissionContextInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 模拟从 Token/会话获取当前用户信息(实际从登录态获取)
        Long userId = Long.parseLong(request.getHeader("userId"));
        Long deptId = Long.parseLong(request.getHeader("deptId"));
        boolean isAdmin = "1".equals(request.getHeader("isAdmin"));

        // 设置数据权限上下文
        DataPermissionContext.DataPermission permission = new DataPermissionContext.DataPermission();
        permission.setUserId(userId);
        permission.setDeptId(deptId);
        permission.setAdmin(isAdmin);
        DataPermissionContext.set(permission);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理上下文,避免 ThreadLocal 内存泄漏
        DataPermissionContext.clear();
    }
}

步骤 5:业务 Mapper(无需修改,插件自动过滤)

xml

复制代码
<!-- SysUserMapper.xml -->
<select id="selectUserList" resultType="com.example.entity.SysUser">
    SELECT * FROM sys_user
</select>

三、责任链执行流程(数据权限插件)

1. 完整链路

plaintext

复制代码
客户端请求 → 
  Spring MVC 拦截器设置数据权限上下文 → 
  MyBatis 执行 selectUserList() → 
    创建 StatementHandler 对象 → 
    Plugin 类为其生成代理(嵌套数据权限插件+日志插件)→ 
    调用 StatementHandler.prepare() → 
    触发责任链执行:
      ① 数据权限插件.intercept() → 
         - 获取权限上下文 → 
         - 解析 SQL → 
         - 拼接 dept_id 条件 → 
         - 调用 invocation.proceed() → 
      ② 日志插件.intercept() → 
         - 打印改写后的 SQL → 
         - 调用 invocation.proceed() → 
    执行原生 StatementHandler.prepare() → 
    执行 SQL(自动过滤当前部门数据)→ 
  Spring MVC 拦截器清理数据权限上下文

2. 执行效果示例

假设当前用户 dept_id = 10,原 SQL:

sql

复制代码
SELECT * FROM sys_user

插件改写后 SQL:

sql

复制代码
SELECT * FROM sys_user WHERE dept_id = 10

若原 SQL 已有 WHERE 条件(如 SELECT * FROM sys_user WHERE status = 1),改写后:

sql

复制代码
SELECT * FROM sys_user WHERE dept_id = 10 AND status = 1

四、MyBatis 责任链在数据权限插件中的核心细节

1. 责任链的 "传递性"

数据权限插件必须调用 invocation.proceed(),否则会中断责任链(原生 SQL 逻辑无法执行)------ 这是 MyBatis 纯责任链的特点(区别于 Spring MVC 可中断的不纯责任链)。

2. 插件顺序的影响

若同时配置了分页插件数据权限插件,需保证:

xml

复制代码
<!-- 正确顺序:数据权限插件先执行(拼接条件)→ 分页插件后执行(基于过滤后的 SQL 分页) -->
<plugins>
    <plugin interceptor="com.example.plugin.DataPermissionInterceptor"/>
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>

若顺序相反,分页插件会先基于原 SQL 分页,再拼接数据权限条件,导致分页结果错误。

3. MetaObject 的核心作用

MyBatis 内部属性(如 BoundSql.sql)是私有的,插件中必须通过 SystemMetaObject.forObject() 获取 MetaObject,而非直接反射 ------ 这是 MyBatis 责任链中 "安全修改目标对象" 的关键。

4. 多租户场景的扩展(责任链的灵活性)

只需修改 buildDataPermissionSql 方法,即可适配多租户数据权限:

java

运行

复制代码
// 多租户 + 部门双重过滤
private String buildDataPermissionSql(String originalSql, DataPermissionContext.DataPermission permission) {
    String tenantCondition = " tenant_id = " + permission.getTenantId();
    String deptCondition = " dept_id = " + permission.getDeptId();
    if (originalSql.toUpperCase().indexOf("WHERE") > 0) {
        return originalSql.replaceFirst("WHERE", "WHERE " + tenantCondition + " AND " + deptCondition + " AND");
    } else {
        return originalSql + " WHERE " + tenantCondition + " AND " + deptCondition;
    }
}

五、数据权限插件的进阶优化(责任链扩展)

1. 支持注解指定过滤规则

在 Mapper 方法上添加注解,精细化控制是否过滤:

java

运行

复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
    boolean enable() default true; // 是否启用数据权限
    String tableAlias() default ""; // 表别名(避免字段冲突)
}

// Mapper 中使用
public interface SysUserMapper {
    @DataPermission(enable = true, tableAlias = "u")
    List<SysUser> selectUserList();
}

// 插件中解析注解
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String mapperId = mappedStatement.getId();
Class<?> mapperClass = Class.forName(mapperId.substring(0, mapperId.lastIndexOf(".")));
String methodName = mapperId.substring(mapperId.lastIndexOf(".") + 1);
Method method = mapperClass.getMethod(methodName);
DataPermission annotation = method.getAnnotation(DataPermission.class);
if (annotation == null || !annotation.enable()) {
    return invocation.proceed(); // 注解禁用,直接放行
}

2. 避免 SQL 改写错误(兼容复杂 SQL)

针对包含 GROUP BY/ORDER BY/JOIN 的复杂 SQL,可通过 SQL 解析器(如 JSqlParser)改写,而非字符串拼接:

java

运行

复制代码
// 引入 JSqlParser 依赖
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.6</version>
</dependency>

// 插件中解析 SQL
private String buildDataPermissionSql(String originalSql, DataPermissionContext.DataPermission permission) throws JSQLParserException {
    Select select = (Select) CCJSqlParserUtil.parse(originalSql);
    PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
    // 构建数据权限条件表达式
    Expression condition = new EqualsTo();
    ((EqualsTo) condition).setLeftExpression(new Column("dept_id"));
    ((EqualsTo) condition).setRightExpression(new LongValue(permission.getDeptId()));
    // 拼接条件
    if (plainSelect.getWhere() == null) {
        plainSelect.setWhere(condition);
    } else {
        plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), condition));
    }
    return select.toString();
}

六、总结:责任链模式在数据权限插件中的价值

  1. 解耦性 :数据权限逻辑与业务 Mapper 完全解耦,无需在每个 SQL 中手写 dept_id = ?,符合 "开闭原则";
  2. 灵活性:新增 / 修改数据权限规则(如按角色过滤),只需修改插件逻辑,无需改动上千个 Mapper;
  3. 可扩展:插件可作为责任链的一环,与分页、日志等插件共存,互不干扰;
  4. 无侵入:基于 MyBatis 插件机制(责任链),无需修改框架源码,适配所有 MyBatis 版本。

MyBatis 的责任链模式让数据权限插件成为 "即插即用" 的组件,是业务中解决 "统一数据权限控制" 的最优方案之一。

相关推荐
syt_10132 小时前
设计模式之-模板模式
设计模式
阿拉斯攀登2 小时前
设计模式:责任链模式(MyBatis)
设计模式·mybatis·责任链模式
无名-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·开发语言·设计模式