插件扩展
MyBatis Plus 提供了一系列强大的内置插件,通过拦截器机制实现对 SQL 执行过程的增强。本章将详细介绍各种插件的使用方法和应用场景。
插件体系概述
什么是插件?
MyBatis Plus 的插件体系基于 MyBatis 的拦截器机制实现。插件可以在 SQL 执行的关键节点(如查询、更新、预处理等)插入自定义逻辑,实现功能的扩展。
简单来说,插件就像一个"过滤器",当 SQL 语句执行时,会先经过插件的拦截处理,然后再真正执行。这种机制让框架可以在不修改原有代码的情况下,灵活地添加新功能。
MybatisPlusInterceptor
MybatisPlusInterceptor 是 MyBatis Plus 插件体系的核心,它代理了 MyBatis 的关键方法,允许在这些方法执行前后插入自定义逻辑。
MyBatis Plus 采用责任链模式管理插件,所有插件都实现 InnerInterceptor 接口。当执行 SQL 时,请求会依次经过每个插件,每个插件都可以对 SQL 进行处理。
这种设计的好处是:
- 开闭原则:新增功能不需要修改框架代码
- 单一职责:每个插件只负责一个功能
- 灵活组合:可以根据需要启用或禁用特定插件
内置插件列表
| 插件 | 说明 | 典型应用场景 |
|---|---|---|
PaginationInnerInterceptor | 分页插件 | 列表分页查询 |
TenantLineInnerInterceptor | 多租户插件 | SaaS 应用数据隔离 |
DynamicTableNameInnerInterceptor | 动态表名插件 | 分表场景 |
OptimisticLockerInnerInterceptor | 乐观锁插件 | 并发更新控制 |
IllegalSQLInnerInterceptor | SQL 性能规范插件 | 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_202401、order_202402 - 多租户分表:每个租户独立表,如
user_tenant1、user_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;
}
}
小结
本章我们学习了:
- 插件体系:理解 MybatisPlusInterceptor 和 InnerInterceptor 的关系
- 多租户插件:实现数据隔离,支持忽略特定表和临时禁用
- 动态表名插件:实现分表场景的表名动态切换
- 数据权限插件:实现行级数据权限控制
- SQL 性能规范插件:检查 SQL 是否符合规范
- 防全表更新删除插件:防止误操作
- 插件组合:多个插件配合使用的配置顺序
合理使用这些插件可以大幅简化开发工作,提高系统安全性和可维护性。