目录
[一、先理清核心: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 代码。
核心思路:
- 数据权限插件作为责任链的具体处理者 ,实现
Interceptor接口; - 拦截
StatementHandler的prepare方法(SQL 构建阶段); - 解析原 SQL,根据当前用户的权限动态拼接数据权限条件;
- 通过
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:实现数据权限插件(责任链的具体处理者)
核心是拦截 StatementHandler 的 prepare 方法,改写 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();
}
六、总结:责任链模式在数据权限插件中的价值
- 解耦性 :数据权限逻辑与业务 Mapper 完全解耦,无需在每个 SQL 中手写
dept_id = ?,符合 "开闭原则"; - 灵活性:新增 / 修改数据权限规则(如按角色过滤),只需修改插件逻辑,无需改动上千个 Mapper;
- 可扩展:插件可作为责任链的一环,与分页、日志等插件共存,互不干扰;
- 无侵入:基于 MyBatis 插件机制(责任链),无需修改框架源码,适配所有 MyBatis 版本。
MyBatis 的责任链模式让数据权限插件成为 "即插即用" 的组件,是业务中解决 "统一数据权限控制" 的最优方案之一。