Spring Boot集成MyBatis-Plus:自定义拦截器实现动态表名切换

Spring Boot集成MyBatis-Plus:自定义拦截器实现动态表名切换


一、引言

介绍动态表名的场景需求,比如多租户系统、分表分库,或者不同业务模块共用一套代码但操作不同表。说明 MyBatis-Plus 默认绑定固定表名的问题。


二、项目配置

1. 集成 MyBatis-Plus

简单说明如何在 Spring Boot 中引入 MyBatis-Plus 并配置。

2. 依赖添加
xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>最新版本号</version>
</dependency>

三、自定义拦截器实现动态表名

1. 拦截器原理

解释 MyBatis 拦截器的核心概念,介绍 Interceptor 接口和 @Signature 注解。

2. 拦截器实现代码

详细展示拦截器的完整实现:

java 复制代码
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicTableInterceptor implements Interceptor {

  	/**
     * 使用ThreadLocal来存储线程特有的数据,这里用于存储动态表名
     */
    private static final ThreadLocal<String> TABLE_NAME_HOLDER = new ThreadLocal<>();

    /**
     * 设置动态表名
     *
     * @param tableName 需要设置的表名,由调用者指定
     *                  此方法允许在运行时动态地设置数据库表名,以便在多数据源或动态表名的场景下灵活地切换表
     */
    public static void setDynamicTableName(String tableName) {
        TABLE_NAME_HOLDER.set(tableName);
    }

    /**
     * 清除当前线程中的动态表名
     * 此方法用于清除ThreadLocal中存储的表名信息,以避免内存泄漏
     * 它应在每次使用动态表名后调用,确保不会对后续的请求产生影响
     */
    public static void clearDynamicTableName() {
        TABLE_NAME_HOLDER.remove();
    }

    /**
     * 拦截器
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();

        String tableName = TABLE_NAME_HOLDER.get();
        if (tableName != null) {
            /**
             * 从实体类的@tableName中获取表名,必须在实体类上面添加@tableName注解
             * 实体类中的@tableName注解中的值,必须是数据库中的表名,否则会报错
             * 例如 :@TableName("t_user_"),数据库是根据月份来的,在这里替换则需要根据当前月份进行拼接表名
             */
            Class<?> entityType = boundSql.getParameterObject().getClass();
            String oldTableName = entityType.getAnnotation(TableName.class).value();
            String newSql = boundSql.getSql().replace(oldTableName, tableName);
            Field sqlField = boundSql.getClass().getDeclaredField("sql");
            sqlField.setAccessible(true);
            sqlField.set(boundSql, newSql);
        }
        return invocation.proceed();
    }
}
3. 使用拦截器动态设置表名
java 复制代码
DynamicTableInterceptor.setDynamicTableName("your_dynamic_table_name");
try {
    myService.saveOrUpdateBatch(entities); // MyBatis-Plus 操作
} finally {
    DynamicTableInterceptor.clearDynamicTableName();
}

四、其他场景

MyBatis 拦截器(Interceptor)是 MyBatis 提供的强大扩展机制,可以拦截执行过程中的不同阶段并进行自定义操作。除了动态修改表名之外,拦截器还可以应用于以下多种场景:


1. SQL 日志记录与审计

  • 场景:记录每次执行的 SQL 语句、参数、执行时间等信息。
  • 用途:用于 SQL 审计、性能监控、问题排查。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = handler.getBoundSql();
    long startTime = System.currentTimeMillis();
    Object result = invocation.proceed();
    long endTime = System.currentTimeMillis();
    System.out.println("SQL: " + boundSql.getSql() + " Execution Time: " + (endTime - startTime) + "ms");
    return result;
}

2. 多租户数据隔离

  • 场景:根据当前租户信息自动添加过滤条件,确保数据隔离。
  • 用途:实现 SaaS 系统中不同租户的数据访问控制。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = handler.getBoundSql();
    String originalSql = boundSql.getSql();
    String tenantId = TenantContext.getCurrentTenantId();
    String modifiedSql = originalSql + " WHERE tenant_id = " + tenantId;
    Field sqlField = boundSql.getClass().getDeclaredField("sql");
    sqlField.setAccessible(true);
    sqlField.set(boundSql, modifiedSql);
    return invocation.proceed();
}

3. SQL 参数加密与解密

  • 场景:对敏感数据(如身份证号、电话等)在 SQL 操作时进行自动加密或解密。
  • 用途:提高数据安全性,确保数据存储符合安全规范。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 自定义加密逻辑
    // 加密处理参数
    return invocation.proceed();
}

4. 自动分页处理

  • 场景:在执行查询时自动添加分页逻辑,避免手动分页处理。
  • 用途:简化分页查询的代码。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = handler.getBoundSql();
    String originalSql = boundSql.getSql();
    String paginatedSql = originalSql + " LIMIT " + offset + ", " + limit;
    Field sqlField = boundSql.getClass().getDeclaredField("sql");
    sqlField.setAccessible(true);
    sqlField.set(boundSql, paginatedSql);
    return invocation.proceed();
}

5. 数据权限控制

  • 场景:根据用户角色或权限,自动添加数据过滤条件。
  • 用途:确保用户只能访问被授权的数据。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = handler.getBoundSql();
    String originalSql = boundSql.getSql();
    String userRole = SecurityContext.getCurrentUserRole();
    String restrictedSql = originalSql + " AND role = '" + userRole + "'";
    // 动态修改 SQL
    return invocation.proceed();
}

6. 缓存增强

  • 场景:自定义缓存逻辑,拦截查询请求,先检查缓存是否命中。
  • 用途:减少数据库访问次数,提高性能。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 检查缓存
    Object cachedResult = CacheManager.get(boundSql.getSql());
    if (cachedResult != null) {
        return cachedResult;
    }
    // 如果没有命中,执行查询
    Object result = invocation.proceed();
    // 存入缓存
    return result;
}

7. 动态数据源切换

  • 场景:根据不同的业务需求动态选择数据源。
  • 用途:实现读写分离或多数据库支持。

示例

java 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
    String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
    DynamicDataSource.setDataSourceKey(dataSourceKey);
    return invocation.proceed();
}

总结

MyBatis 拦截器为开发者提供了灵活的扩展能力,可以在 SQL 执行的多个阶段中注入自定义逻辑,从而实现多种高级功能。合理使用拦截器不仅能增强系统功能,还能提升性能和安全性。

相关推荐
Victor3562 分钟前
Redis(6)Redis的单线程模型是如何工作的?
后端
Victor3563 分钟前
Redis(7)Redis如何实现高效的内存管理?
后端
David爱编程1 小时前
进程 vs 线程到底差在哪?一文吃透操作系统视角与 Java 视角的关键差异
后端
smileNicky11 小时前
SpringBoot系列之从繁琐配置到一键启动之旅
java·spring boot·后端
David爱编程12 小时前
为什么必须学并发编程?一文带你看懂从单线程到多线程的演进史
java·后端
long31612 小时前
java 策略模式 demo
java·开发语言·后端·spring·设计模式
rannn_11113 小时前
【Javaweb学习|黑马笔记|Day1】初识,入门网页,HTML-CSS|常见的标签和样式|标题排版和样式、正文排版和样式
css·后端·学习·html·javaweb
柏油14 小时前
Spring @Cacheable 解读
redis·后端·spring
柏油14 小时前
Spring @TransactionalEventListener 解读
spring boot·后端·spring
小小工匠15 小时前
Maven - Spring Boot 项目打包本地 jar 的 3 种方法
spring boot·maven·jar·system scope