跳到主要内容

插件扩展

MyBatis Plus 提供了一系列强大的内置插件,通过拦截器机制实现对 SQL 执行过程的增强。本章将详细介绍各种插件的使用方法和应用场景。

插件体系概述

什么是插件?

MyBatis Plus 的插件体系基于 MyBatis 的拦截器机制实现。插件可以在 SQL 执行的关键节点(如查询、更新、预处理等)插入自定义逻辑,实现功能的扩展。

简单来说,插件就像一个"过滤器",当 SQL 语句执行时,会先经过插件的拦截处理,然后再真正执行。这种机制让框架可以在不修改原有代码的情况下,灵活地添加新功能。

MybatisPlusInterceptor

MybatisPlusInterceptor 是 MyBatis Plus 插件体系的核心,它代理了 MyBatis 的关键方法,允许在这些方法执行前后插入自定义逻辑。

MyBatis Plus 采用责任链模式管理插件,所有插件都实现 InnerInterceptor 接口。当执行 SQL 时,请求会依次经过每个插件,每个插件都可以对 SQL 进行处理。

这种设计的好处是:

  1. 开闭原则:新增功能不需要修改框架代码
  2. 单一职责:每个插件只负责一个功能
  3. 灵活组合:可以根据需要启用或禁用特定插件

内置插件列表

插件说明典型应用场景
PaginationInnerInterceptor分页插件列表分页查询
TenantLineInnerInterceptor多租户插件SaaS 应用数据隔离
DynamicTableNameInnerInterceptor动态表名插件分表场景
OptimisticLockerInnerInterceptor乐观锁插件并发更新控制
IllegalSQLInnerInterceptorSQL 性能规范插件SQL 审计
BlockAttackInnerInterceptor防止全表更新删除插件数据安全保护
DataPermissionInterceptor数据权限插件行级数据权限控制

插件配置方式

在 Spring Boot 中配置插件,需要创建配置类:

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

// 添加防止全表更新删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

return interceptor;
}
}

注意:插件的添加顺序很重要。建议顺序为:多租户 → 动态表名 → 分页 → 乐观锁 → SQL 性能规范 → 防全表更新删除。

多租户插件

什么是多租户?

多租户(Multi-Tenancy)是一种软件架构模式,允许一个应用实例为多个租户(客户)提供服务,每个租户的数据相互隔离。这在 SaaS 应用中非常常见。

数据隔离有三种主要方式:

隔离方式说明优点缺点
独立数据库每个租户一个数据库隔离性最好成本高
共享数据库,独立 Schema每个租户一个 Schema平衡隔离与成本跨租户查询复杂
共享数据库,共享 Schema通过租户 ID 字段区分成本最低需要严格过滤

MyBatis Plus 的多租户插件适用于第三种方式,通过自动为 SQL 添加租户 ID 条件实现数据隔离。

基本配置

第一步:实现租户处理器

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class CustomTenantHandler implements TenantLineHandler {

/**
* 获取租户 ID 值
* 实际项目中通常从登录上下文获取当前用户的租户 ID
*/
@Override
public Expression getTenantId() {
// 从上下文获取当前租户 ID
Long tenantId = TenantContextHolder.getCurrentTenantId();
return new LongValue(tenantId);
}

/**
* 获取租户字段名
* 默认为 tenant_id
*/
@Override
public String getTenantIdColumn() {
return "tenant_id";
}

/**
* 忽略的表
* 返回 true 表示该表不需要拼接租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
// 系统表、字典表等不需要租户隔离
List<String> ignoreTables = Arrays.asList(
"sys_user",
"sys_role",
"sys_dict",
"sys_config"
);
return ignoreTables.contains(tableName.toLowerCase());
}
}

第二步:配置插件

@Configuration
public class MybatisPlusConfig {

@Autowired
private CustomTenantHandler customTenantHandler;

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(customTenantHandler);

interceptor.addInnerInterceptor(tenantInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;
}
}

租户上下文管理

实际项目中需要一个地方存储当前请求的租户 ID,通常使用 ThreadLocal 实现:

/**
* 租户上下文工具类
*/
public class TenantContextHolder {

private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>();

/**
* 设置当前租户 ID
*/
public static void setCurrentTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}

/**
* 获取当前租户 ID
*/
public static Long getCurrentTenantId() {
Long tenantId = TENANT_ID.get();
if (tenantId == null) {
throw new RuntimeException("未设置租户 ID");
}
return tenantId;
}

/**
* 清除租户 ID
*/
public static void clear() {
TENANT_ID.remove();
}
}

在拦截器中设置租户 ID:

@Component
public class TenantInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头获取租户 ID
String tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.isNotBlank(tenantId)) {
TenantContextHolder.setCurrentTenantId(Long.parseLong(tenantId));
}
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理租户上下文
TenantContextHolder.clear();
}
}

忽略租户条件

某些场景需要绕过租户过滤,例如管理员查看所有租户数据。

方式一:使用注解

import com.baomidou.mybatisplus.extension.plugins.interceptor.IInterceptorIgnore;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper extends BaseMapper<User> {

/**
* 查询所有租户的用户(管理员用)
* @InterceptorIgnore 注解可以忽略指定插件
*/
@InterceptorIgnore(tenantLine = "true")
@Select("SELECT * FROM user WHERE status = #{status}")
List<User> selectAllTenantUsers(@Param("status") Integer status);
}

方式二:手动控制

import com.baomidou.mybatisplus.extension.plugins.interceptor.IInterceptorIgnore;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptorUtils;

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

/**
* 管理员查询所有租户数据
*/
public List<User> listAllUsers() {
// 临时忽略租户插件
try {
InterceptorIgnoreHelper.handle(
IgnoreStrategy.builder().tenantLine(true).build()
);
return userMapper.selectList(null);
} finally {
// 必须在 finally 中清除忽略策略
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
}
}

插入时自动填充租户字段

多租户插件只处理查询条件,插入数据时需要配合自动填充填充租户字段:

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
// 自动填充租户 ID
this.strictInsertFill(metaObject, "tenantId", Long.class,
TenantContextHolder.getCurrentTenantId());
}

@Override
public void updateFill(MetaObject metaObject) {
// 更新时也可以填充
}
}

实体类中配置:

@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;

private String name;

@TableField(fill = FieldFill.INSERT)
private Long tenantId;
}

动态表名插件

应用场景

动态表名插件适用于以下场景:

  • 分表存储:按日期、地区等维度分表,如订单表按月分表 order_202401order_202402
  • 多租户分表:每个租户独立表,如 user_tenant1user_tenant2
  • 历史数据归档:历史数据存储在独立表中

基本配置

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

DynamicTableNameInnerInterceptor dynamicTableNameInterceptor =
new DynamicTableNameInnerInterceptor();

// 设置动态表名处理器
dynamicTableNameInterceptor.setTableNameHandler((sql, tableName) -> {
// 如果是订单表,根据日期动态切换
if ("t_order".equals(tableName)) {
String yearMonth = OrderTableContext.getCurrentYearMonth();
return tableName + "_" + yearMonth;
}
return tableName;
});

interceptor.addInnerInterceptor(dynamicTableNameInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;
}
}

按日期分表示例

定义表名上下文:

/**
* 订单表名上下文
*/
public class OrderTableContext {

private static final ThreadLocal<String> YEAR_MONTH = new ThreadLocal<>();

public static void setYearMonth(String yearMonth) {
YEAR_MONTH.set(yearMonth);
}

public static String getCurrentYearMonth() {
String yearMonth = YEAR_MONTH.get();
if (yearMonth == null) {
// 默认当前月份
return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
}
return yearMonth;
}

public static void clear() {
YEAR_MONTH.remove();
}
}

使用示例:

@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

/**
* 查询指定月份的订单
*/
public List<Order> getOrdersByMonth(String yearMonth) {
try {
OrderTableContext.setYearMonth(yearMonth);
return orderMapper.selectList(null);
} finally {
OrderTableContext.clear();
}
}

/**
* 创建订单
*/
public void createOrder(Order order) {
// 订单创建在当月表中
orderMapper.insert(order);
}
}

生成的 SQL 会自动替换表名:

-- 原始 SQL
SELECT * FROM t_order WHERE id = 1

-- 替换后的 SQL(假设当前是 2024年1月)
SELECT * FROM t_order_202401 WHERE id = 1

多租户分表示例

dynamicTableNameInterceptor.setTableNameHandler((sql, tableName) -> {
if ("t_user".equals(tableName)) {
Long tenantId = TenantContextHolder.getCurrentTenantId();
return tableName + "_tenant" + tenantId;
}
return tableName;
});

数据权限插件

什么是数据权限?

数据权限控制用户能访问哪些数据行,通常基于组织架构、角色等维度。例如,销售经理只能看到自己部门的数据,普通员工只能看到自己的数据。

配置数据权限插件

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import org.springframework.stereotype.Component;

@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 数据权限插件
DataPermissionInterceptor dataPermissionInterceptor =
new DataPermissionInterceptor(new CustomDataPermissionHandler());

interceptor.addInnerInterceptor(dataPermissionInterceptor);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;
}
}

/**
* 自定义数据权限处理器
*/
@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {

@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 获取当前用户的数据权限范围
User currentUser = SecurityUtils.getCurrentUser();

if (currentUser == null) {
return where;
}

// 根据用户角色构建权限条件
Expression permissionExpression = null;

if ("ADMIN".equals(currentUser.getRole())) {
// 管理员可以查看所有数据
return where;
} else if ("DEPT_MANAGER".equals(currentUser.getRole())) {
// 部门经理只能查看本部门数据
permissionExpression = buildDeptExpression(currentUser.getDeptId());
} else {
// 普通用户只能查看自己的数据
permissionExpression = buildUserExpression(currentUser.getId());
}

// 将权限条件拼接到原有条件
if (where == null) {
return permissionExpression;
}
return new AndExpression(where, permissionExpression);
}

/**
* 构建部门权限条件
*/
private Expression buildDeptExpression(Long deptId) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("dept_id"));
equalsTo.setRightExpression(new LongValue(deptId));
return equalsTo;
}

/**
* 构建用户权限条件
*/
private Expression buildUserExpression(Long userId) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("user_id"));
equalsTo.setRightExpression(new LongValue(userId));
return equalsTo;
}
}

SQL 性能规范插件

IllegalSQLInnerInterceptor 可以检查 SQL 是否符合规范,防止不规范的 SQL 影响性能。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 添加 SQL 性能规范插件
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());

interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;
}

该插件会检查以下问题:

  • 是否使用了 SELECT *
  • 是否没有使用索引
  • WHERE 条件是否合理
  • 是否有笛卡尔积

防止全表更新删除插件

BlockAttackInnerInterceptor 可以防止误操作导致的全表更新或删除。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 添加防全表更新删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;
}

当执行没有 WHERE 条件的 UPDATE 或 DELETE 时,会抛出异常:

Prohibition of full table update operation

如果确实需要全表操作,可以临时禁用:

@Service
public class DataCleanupService {

@Autowired
private UserMapper userMapper;

public void cleanTempData() {
// 临时禁用防全表更新插件
try {
InterceptorIgnoreHelper.handle(
IgnoreStrategy.builder().blockAttack(true).build()
);
userMapper.delete(null);
} finally {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
}
}

本地缓存 SQL 解析

为了提高性能,可以启用 SQL 解析缓存:

import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import java.util.concurrent.TimeUnit;

public class Application {
static {
// 设置 SQL 解析缓存
JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
cache -> cache.maximumSize(1024)
.expireAfterWrite(5, TimeUnit.SECONDS)
));
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

插件组合使用

实际项目中通常需要组合多个插件,完整配置示例:

@Configuration
public class MybatisPlusConfig {

@Autowired
private CustomTenantHandler customTenantHandler;

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 1. 多租户插件(最先执行)
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(customTenantHandler);
interceptor.addInnerInterceptor(tenantInterceptor);

// 2. 动态表名插件
DynamicTableNameInnerInterceptor dynamicTableNameInterceptor =
new DynamicTableNameInnerInterceptor();
dynamicTableNameInterceptor.setTableNameHandler(this::dynamicTableNameHandler);
interceptor.addInnerInterceptor(dynamicTableNameInterceptor);

// 3. 分页插件
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInterceptor);

// 4. 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

// 5. SQL 性能规范插件
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());

// 6. 防全表更新删除插件(最后执行)
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

return interceptor;
}

private String dynamicTableNameHandler(String sql, String tableName) {
// 动态表名处理逻辑
return tableName;
}
}

小结

本章我们学习了:

  1. 插件体系:理解 MybatisPlusInterceptor 和 InnerInterceptor 的关系
  2. 多租户插件:实现数据隔离,支持忽略特定表和临时禁用
  3. 动态表名插件:实现分表场景的表名动态切换
  4. 数据权限插件:实现行级数据权限控制
  5. SQL 性能规范插件:检查 SQL 是否符合规范
  6. 防全表更新删除插件:防止误操作
  7. 插件组合:多个插件配合使用的配置顺序

合理使用这些插件可以大幅简化开发工作,提高系统安全性和可维护性。

参考资源