MyBatis-Plus前置拦截器,基于注解按需分表方案

🌟概述

在实际业务中,并不是所有表都需要做分表处理。因此我们结合自定义注解与 MyBatis 拦截器机制,优雅地实现了按需分表查询的功能。

本篇文章聚焦于实现逻辑,不深入探讨 MyBatis-plus 的底层机制,重点介绍如何在使用 LambdaQueryWrapper 查询时,实现自动分表。

👉 业务背景:本例场景仅需将数据划分为“今日表”和“历史表”。如果你有更多维度的分表需求,可在此基础上扩展 SQL 替换逻辑。

🎯 完整代码是1,2,4章节,第3章主要是分步详细解析,有基础的可以直接看完整代码,对疑惑的点在看详细的解析

🌟介绍

🎯 目标

当查询命中某一条件(如时间早于今日),自动将 SQL 中的表名替换为指定的历史表名,实现数据的逻辑分表查询。

🔄 实现流程

整个逻辑分为四个核心步骤:

  1. 使用 自定义注解 标记需要支持分表的实体类;

  2. 启动时或通过定时任务自动 创建历史表并迁移数据

  3. 拦截 SQL 执行前,判断是否 命中历史表查询条件

  4. 若命中,动态替换表名,执行分表查询。

1、自定义注解

主要有,分表后缀,分表字段。

注:因为我的业务需求只需要分一个历史数据表。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author 崔
 * @Date:2025/04/26 14:18
 * @vesion: 0.0.1
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableSplit {

    boolean split() default true;

    //分表的后缀
    String tableSuffix() default "";
    //分表字段,根据哪个字段分,查询的时候如果有这个字段的条件,就会走对应的逻辑
    String fieldName() default "create_date_time";
}

📌 说明:本业务场景只做了一个“历史表”拆分,字段默认为 create_date_time,你可以按需修改。

2、mybatis-plus拦截器

我们通过实现 MyBatis-plus 的 Interceptor 接口,对 SQL 进行预处理和后处理。

1、@Intercepts 注解,就是启用mybatis监听

2、@Signature 注解就是说,我是查询/修改/新增哪些节点去才拦截

3、SqlListenerHandel 自定义类,里面有2个方法,前置处理,后置处理

实现流程,通过mybatis-plus的注解,进入拦截类,然后自己写了一个工具类,在里面处理他的sql,最后执行

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;

/**
 * SQL拆分监听器拦截器
 * 用于拦截MyBatis执行过程中的SQL语句,并在执行前后进行自定义处理
 * 主要拦截 StatementHandler.prepare、Executor.query、Executor.update 三种方法
 *
 * @author 崔
 * @date 2025/04/26
 * @version 0.0.1
 */
@Setter
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class SqlListenerInterceptor implements Interceptor {

    /**
     * SQL监听处理器,用于自定义前置/后置处理逻辑
     */
    private SqlListenerHandel sqlListenerHandel;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis(); // 记录执行开始时间

        // 调用前置处理方法,分表
        sqlListenerHandel.beforeHandel(invocation);

        String sql = ""; // 初始化SQL变量

        // 根据拦截的方法不同,提取对应的SQL语句
        if (invocation.getMethod().getName().equals("prepare")) {
            // 拦截的是StatementHandler.prepare方法
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = statementHandler.getBoundSql(); // 获取绑定的SQL对象
            sql = boundSql.getSql(); // 提取SQL语句
        } else if (invocation.getMethod().getName().equals("query")) {
            // 拦截的是Executor.query方法
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            sql = ms.getBoundSql(parameter).getSql(); // 提取SQL语句
        } else if (invocation.getMethod().getName().equals("update")) {
            // 拦截的是Executor.update方法
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            sql = ms.getBoundSql(parameter).getSql(); // 提取SQL语句
        }

        // 执行原方法(即执行真实的SQL操作)
        Object result = invocation.proceed();

        // 调用后置处理方法(可以在这里做SQL执行结果分析、日志等)
        sqlListenerHandel.afterHandel(invocation);

        long endTime = System.currentTimeMillis(); // 记录执行结束时间

        // 打印SQL执行时间
        log.info(" ==> execTime: " + (endTime - startTime) + " ms");

        // 如果SQL执行时间超过5秒,记录成错误日志,便于排查慢SQL
        if ((endTime - startTime) > 5000) {
            log.error("sql: " + sql + "; 耗时" + (endTime - startTime) + " ms");
        }

        return result; // 返回执行结果
    }

}

3、核心处理逻辑

3.1 拦截逻辑判断

  • 只拦截 SQL 准备阶段(prepare 方法)。

  • 通过反射获取 SQL 类型:SELECT / INSERT / UPDATE / DELETE

  • 当前只处理 SELECT 语句。

    public void beforeHandel(Invocation invocation) {
        if (!invocation.getMethod().getName().equals("prepare")) {
            return;
        }
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        String typeName = String.valueOf(metaObject.getValue("h.target.delegate.parameterHandler.sqlCommandType.name"));

        if ("SELECT".equals(typeName)) {
            selectBeforeHandel(statementHandler);
        } else if ("INSERT".equals(typeName)) {

        }
    }

 3.2 分表 SQL 替换逻辑

🧠 功能总结:

当查询的实体类标注了 @TableSplit 注解,并且查询条件中包含指定的时间字段,且时间早于今天,就会自动将 SQL 中的表名替换为历史表名

🔍 分步骤解析:

  1. 通过反射拿到查询实体类类型

    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("h.target.delegate.mappedStatement");
    Class<?> c = mappedStatement.getResultMaps().get(0).getType();

    用于获取查询的实体类,从而通过注解获取到其他信息

  2. 判断是否需要分表

    TableSplit tableSplit = (TableSplit) c.getAnnotation(TableSplit.class);
    if (tableSplit == null) {
        return;
    }
    f (StringUtil.isEmpty(tableName) || tableName.contains(tableSplit.tableSuffix())) {
        return;
    }

    如果类上没有 @TableSplit 注解,说明当前表不支持分表,直接返回。

  3. 参数合法性校验

    Object o = statementHandler.getParameterHandler().getParameterObject();
    if (!(o instanceof MapperMethod.ParamMap)) {//查询
        return;
    }
    MapperMethod.ParamMap<Object> parameterObject = (MapperMethod.ParamMap<Object>) o;
    if (!(parameterObject.containsKey("ew") && parameterObject.get("ew") instanceof LambdaQueryWrapper)) {
        return;
    }

    仅支持使用 LambdaQueryWrapper 进行构建的查询(通常是 MP 的写法)。

  4. 是否命中历史分表的条件

    //查询条件
    boolean splitFlag = splitFlag(lambdaQueryWrapper, tableSplit);
    if (!splitFlag) {
        return;
    }        

    分析 SQL 中的查询字段和时间值,判断是否应该使用历史表。

  5. 执行 SQL 替换表名

    String sql = statementHandler.getBoundSql().getSql();
    String newSql = sql.replace(tableName, tableName + tableSplit.tableSuffix());
    metaObject.setValue("h.target.delegate.boundSql.sql", newSql);

    如果满足分表条件,将 SQL 中的表名替换为带后缀的新表名,实际执行时就会访问历史表。

3.3 条件匹配逻辑(是否切换历史表)

这个方法的作用是:根据查询条件中的字段和值判断,当前查询是否应该切换到历史表。

🚦 核心判断逻辑:

只有在查询中包含了 @TableSplit 注解指定的字段(通常是时间字段),并且该时间早于今天,才认为是对历史数据的查询。

🔍 分步骤解析

1. 获取条件表达式相关信息
Map<String, Object> paramMap = lambdaQueryWrapper.getParamNameValuePairs();
String sqlSegment = lambdaQueryWrapper.getExpression().getSqlSegment();
  • paramMap:封装了所有查询条件中用到的字段和值。字段是被从新加载的。

  • sqlSegment:条件的原始 SQL 片段,例如:date < #{ew.paramNameValuePairs.date}。最后面的和上面paramMap对应

 2. 匹配字段与值的对应关系
//获取本次包含的条件(= != AND IN ....)
final String normalStr = lambdaQueryWrapper.getExpression().getNormal().stream().filter(item -> item instanceof SqlKeyword).map(item -> item.getSqlSegment()).distinct().collect(Collectors.joining(""));
final Pattern pattern = Pattern.compile("(\\w+)\\s*[" + normalStr + "]+\\s*#\\{ew\\.paramNameValuePairs\\.(\\w+)\\}");
final Pattern patternParam = Pattern.compile("\\s*[" + normalStr + "]+\\s*");
final Matcher matcher = pattern.matcher(sqlSegment);

使用正则匹配从 SQL 中提取,就可以获取到每一个查询条件对应的值

对于相同的key,获取最小值,因我我的需求是如果查了历史的,就不能查今日。做了拆分

这些信息会被用于后续判断

 3.识别是否命中历史表条件
if (mappingValueMap.containsKey(tableSplit.fieldName()) && ((DateUtils.getDate("yyyy-MM-dd") + " 00:00:00").compareTo(mappingValueMap.get(tableSplit.fieldName()).toString()) > 0 || mappingSegmentMap.containsKey(tableSplit.fieldName()))) {
    return true;
}

最后根据条件判断是否查询历史的表

4、完整代码

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.enums.SqlKeyword;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * sql拆分监听器,看查询哪个表
 *
 * @author 崔
 * @Date:2024/11/2 12:39
 * @vesion: 0.0.1
 */
@Component
public class SqlListenerHandel {

    private static final Logger log = LoggerFactory.getLogger(SqlListenerHandel.class);
    //前置拦截
    public void beforeHandel(Invocation invocation) {
        //不是执行前的结束
        if (!invocation.getMethod().getName().equals("prepare")) {
            return;
        }
        //通过反射获取到当前sql类型,SELECT/INSERT....
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        String typeName = String.valueOf(metaObject.getValue("h.target.delegate.parameterHandler.sqlCommandType.name"));

        if ("SELECT".equals(typeName)) {
            selectBeforeHandel(statementHandler);
        } else if ("INSERT".equals(typeName)) {

        }
    }

    /**
     * 查询拦截器
     *
     * @param statementHandler
     */
    public void selectBeforeHandel(StatementHandler statementHandler) {
        //通过反射获取到查询的实体类
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("h.target.delegate.mappedStatement");
        Class<?> c = mappedStatement.getResultMaps().get(0).getType();
        String tableName = CreateUtil.getTableName(c);

        //当前实体类是否有规定注解
        TableSplit tableSplit = (TableSplit) c.getAnnotation(TableSplit.class);
        if (tableSplit == null) {
            return;
        }
        if (StringUtil.isEmpty(tableName) || tableName.contains(tableSplit.tableSuffix())) {
            return;
        }

        //是否是查询
        Object o = statementHandler.getParameterHandler().getParameterObject();
        if (!(o instanceof MapperMethod.ParamMap)) {//查询
            return;
        }

        //获取查询条件k和v
        MapperMethod.ParamMap<Object> parameterObject = (MapperMethod.ParamMap<Object>) o;
        if (!(parameterObject.containsKey("ew") && parameterObject.get("ew") instanceof LambdaQueryWrapper)) {
            return;
        }
        final LambdaQueryWrapper<?> lambdaQueryWrapper = (LambdaQueryWrapper<?>) parameterObject.get("ew");
        //查询条件 根据条件判断是否要修改查询的表名
        boolean splitFlag = splitFlag(lambdaQueryWrapper, tableSplit);
        if (!splitFlag) {
            return;
        }
        //修改掉查询的表名,完成拦截
        String sql = statementHandler.getBoundSql().getSql();
        String newSql = sql.replace(tableName, tableName + tableSplit.tableSuffix());
        metaObject.setValue("h.target.delegate.boundSql.sql", newSql);
    }

    /**
     * 判断符不符合查询历史表的要求
     *
     * @param lambdaQueryWrapper
     * @return
     */
    public boolean splitFlag(final LambdaQueryWrapper<?> lambdaQueryWrapper, TableSplit tableSplit) {
        try {
            final Map<String, Object> mappingValueMap = Maps.newHashMap();
            final Map<String, Object> mappingSegmentMap = Maps.newHashMap();
            //获取到查询条件
            final Map<String, Object> paramMap = lambdaQueryWrapper.getParamNameValuePairs();
            final String sqlSegment = lambdaQueryWrapper.getExpression().getSqlSegment();
            //获取本次包含的条件(= != AND IN ....)
            final String normalStr = lambdaQueryWrapper.getExpression().getNormal().stream().filter(item -> item instanceof SqlKeyword).map(item -> item.getSqlSegment()).distinct().collect(Collectors.joining(""));
            final Pattern pattern = Pattern.compile("(\\w+)\\s*[" + normalStr + "]+\\s*#\\{ew\\.paramNameValuePairs\\.(\\w+)\\}");
            final Pattern patternParam = Pattern.compile("\\s*[" + normalStr + "]+\\s*");
            final Matcher matcher = pattern.matcher(sqlSegment);
            List<String> les = Lists.newArrayList("<","<=");
            while (matcher.find()) {
                String sqlMatch = matcher.group(0);
                String key = matcher.group(1); // 提取键
                String valueKey = matcher.group(2);
                Object value = paramMap.get(valueKey); // 提取值
                //相同的key获取值比较小的,比如时间,获取最小的时间
                if (mappingValueMap.containsKey(key) && mappingValueMap.get(key).toString().compareTo(String.valueOf(value)) < 0) {
                    continue;
                }
                Matcher matcherParam = patternParam.matcher(sqlMatch);
                while (matcherParam.find()) {
                    String segment = matcherParam.group(0).trim();
                    if (les.contains(segment)) {
                        mappingSegmentMap.put(key, segment);
                    }
                }
                mappingValueMap.put(key, value);
            }
            //判断规则的key是否满足条件
            if (mappingValueMap.containsKey(tableSplit.fieldName())
                    && ((DateUtils.getDate("yyyy-MM-dd") + " 00:00:00").compareTo(mappingValueMap.get(tableSplit.fieldName()).toString()) > 0
                    || mappingSegmentMap.containsKey(tableSplit.fieldName()))
            ) {
                return true;
            }
        } catch (Exception e) {
            log.error("分表判断异常", e);
            return false;
        }
        return false;
    }
}

✅总结

  • 本方案通过 MyBatis 拦截器机制实现 SQL 的动态分表,逻辑清晰、扩展性强;

  • 目前支持 LambdaQueryWrapper 查询方式,其他写法可按需扩展;

  • 若你的业务需要更复杂的分表维度,只需拓展 splitFlag()replaceTableName() 方法逻辑即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值