千家信息网

用Mybatis手写一个分表插件

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章主要介绍"用Mybatis手写一个分表插件",在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"用Myba
千家信息网最后更新 2026年11月03日用Mybatis手写一个分表插件

这篇文章主要介绍"用Mybatis手写一个分表插件",在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"用Mybatis手写一个分表插件"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

背景

事情是酱紫的,阿星的上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。

上级leader完成业务的开发后,走了一波自测,git push后,就忙其他的事情去了。

项目的框架是SpringBoot+Mybaits

出问题了

阿星负责的业务也开发完了,熟练的git pull,准备自测,单元测试run一下,上个厕所回来收工,就是这么自信。

回来后,看下控制台,人都傻了,一片红,内心不禁感叹"如果这是股票基金该多好"。

出了问题就要解决,随着排查深入,我的眉头一皱发现事情并不简单,怎么以前的一些代码都报错了?

随着排查深入,最后跟到了Mybatis源码,发现罪魁祸首是sharding-jdbc引起的,因为数据源是sharding-jdbc的,导致后续执行sql的是ShardingPreparedStatement。

这就意味着,sharding-jdbc影响项目的所有业务表,因为最终数据库交互都由ShardingPreparedStatement去做了,历史的一些sql语句因为sql函数或者其他写法,使得ShardingPreparedStatement无法处理而出现异常。

关键代码如下

发现问题后,阿星马上就反馈给leader了。

唉,本来还想摸鱼的,看来摸鱼的时间是没了,还多了一项任务。

分析

竟然交给阿星来做了,就撸起袖子开干吧,先看看分表功能的需求

  • 支持自定义分表策略

  • 能控制影响范围

  • 通用性

分表会提前建立好,所以不需要考虑表不存在的问题,核心逻辑实现,通过分表策略得到分表名,再把分表名动态替换到sql。

分表策略

为了支持分表策略,我们需要先定义分表策略抽象接口,定义如下

/**  * @Author 程序猿阿星  * @Description 分表策略接口  * @Date 2021/5/9  */ public interface ITableShardStrategy {       /**      * @author: 程序猿阿星      * @description: 生成分表名      * @param tableNamePrefix 表前缀名      * @param value 值      * @date: 2021/5/9      * @return: java.lang.String      */     String generateTableName(String tableNamePrefix,Object value);      /**      * 验证tableNamePrefix      */     default void verificationTableNamePrefix(String tableNamePrefix){         if (StrUtil.isBlank(tableNamePrefix)) {             throw new RuntimeException("tableNamePrefix is null");         }     } }

generateTableName函数的任务就是生成分表名,入参有tableNamePrefix、value,tableNamePrefix为分表前缀,value作为生成分表名的逻辑参数。

verificationTableNamePrefix函数验证tableNamePrefix必填,提供给实现类使用。

为了方便理解,下面是id取模策略代码,取模两张表

/**  * @Author 程序猿阿星  * @Description 分表策略id  * @Date 2021/5/9  */ @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //此处可以缓存优化         return tableNamePrefix + "_" + (id % 2);     } }

传入进来的value是id值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影响范围

分表策略已经抽象出来,下面要考虑控制影响范围,我们都知道Mybatis规范中每个Mapper类对应一张业务主体表,Mapper类的函数对应业务主体表的相关sql。

阿星想着,可以给Mapper类打上注解,代表该Mpaaer类对应的业务主体表有分表需求,从规范来说Mapper类的每个函数对应的主体表都是正确的,但是有些同学可能不会按规范来写。

假设Mpaaer类对应的是B表,Mpaaer类的某个函数写着A表的sql,甚至是历史遗留问题,所以注解不仅仅可以打在Mapper类上,同时还可以打在Mapper类的任意一个函数上,并且保证小粒度覆盖粗粒度。

阿星这里自定义分表注解,代码如下

/**  * @Author 程序猿阿星  * @Description 分表注解  * @Date 2021/5/9  */ @Target(value = {ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TableShard {      // 表前缀名     String tableNamePrefix();      //值     String value() default "";      //是否是字段名,如果是需要解析请求参数改字段名的值(默认否)     boolean fieldFlag() default false;      // 对应的分表策略类     Class shardStrategy();   }

注解的作用范围是类、接口、函数,运行时生效。

tableNamePrefix与shardStrategy属性都好理解,表前缀名和分表策略,剩下的value与fieldFlag要怎么理解,分表策略分两类,第一类依赖表中某个字段值,第二类则不依赖。

根据企业id取模,属于第一类,此处的value设置企业id入参字段名,fieldFlag为true,意味着,会去解析获取企业id字段名对应的值。

根据日期分表,属于第二类,直接在分表策略实现类里面写就行了,不依赖表字段值,value与fieldFlag无需填写,当然你value也可以设置时间格式,具体看分表策略实现类的逻辑。

通用性

抽象分表策略与分表注解都搞定了,最后一步就是根据分表注解信息,去执行分表策略得到分表名,再把分表名动态替换到sql中,同时具有通用性。

Mybatis框架中,有拦截器机制做扩展,我们只需要拦截StatementHandler#prepare函数,即StatementHandle创建Statement之前,先把sql里面的表名动态替换成分表名。

Mybatis分表拦截器流程图如下

Mybatis分表拦截器代码如下,有点长哈,主流程看intercept函数就好了。

/**  * @Author 程序员阿星  * @Description 分表拦截器  * @Date 2021/5/9  */ @Intercepts({         @Signature(                 type = StatementHandler.class,                 method = "prepare",                 args = {Connection.class, Integer.class}         ) }) public class TableShardInterceptor implements Interceptor {      private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();      @Override     public Object intercept(Invocation invocation) throws Throwable {          // MetaObject是mybatis里面提供的一个工具类,类似反射的效果         MetaObject metaObject = getMetaObject(invocation);         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");         MappedStatement mappedStatement = (MappedStatement)                 metaObject.getValue("delegate.mappedStatement");          //获取Mapper执行方法         Method method = invocation.getMethod();          //获取分表注解         TableShard tableShard = getTableShard(method,mappedStatement);          // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑         if (tableShard == null) {             return invocation.proceed();         }          //获取值         String value = tableShard.value();         //value是否字段名,如果是,需要解析请求参数字段名的值         boolean fieldFlag = tableShard.fieldFlag();          if (fieldFlag) {             //获取请求参数             Object parameterObject = boundSql.getParameterObject();              if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap类型逻辑处理                  MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;                 //根据字段名获取参数值                 Object valueObject = parameterMap.get(value);                 if (valueObject == null) {                     throw new RuntimeException(String.format("入参字段%s无匹配", value));                 }                 //替换sql                 replaceSql(tableShard, valueObject, metaObject, boundSql);              } else { //单参数逻辑                  //如果是基础类型抛出异常                 if (isBaseType(parameterObject)) {                     throw new RuntimeException("单参数非法,请使用@Param注解");                 }                  if (parameterObject instanceof Map){                     Map  parameterMap =  (Map)parameterObject;                     Object valueObject = parameterMap.get(value);                     //替换sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 } else {                     //非基础类型对象                     Class parameterObjectClass = parameterObject.getClass();                     Field declaredField = parameterObjectClass.getDeclaredField(value);                     declaredField.setAccessible(true);                     Object valueObject = declaredField.get(parameterObject);                     //替换sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 }             }          } else {//无需处理parameterField             //替换sql             replaceSql(tableShard, value, metaObject, boundSql);         }         //执行下一个插件逻辑         return invocation.proceed();     }       @Override     public Object plugin(Object target) {         // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数         if (target instanceof StatementHandler) {             return Plugin.wrap(target, this);         } else {             return target;         }     }       /**      * @param object      * @methodName: isBaseType      * @author: 程序员阿星      * @description: 基本数据类型验证,true是,false否      * @date: 2021/5/9      * @return: boolean      */     private boolean isBaseType(Object object) {         if (object.getClass().isPrimitive()                 || object instanceof String                 || object instanceof Integer                 || object instanceof Double                 || object instanceof Float                 || object instanceof Long                 || object instanceof Boolean                 || object instanceof Byte                 || object instanceof Short) {             return true;         } else {             return false;         }     }      /**      * @param tableShard 分表注解      * @param value      值      * @param metaObject mybatis反射对象      * @param boundSql   sql信息对象      * @author: 程序猿阿星      * @description: 替换sql      * @date: 2021/5/9      * @return: void      */     private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {         String tableNamePrefix = tableShard.tableNamePrefix();         //获取策略class         Class strategyClazz = tableShard.shardStrategy();         //从spring ioc容器获取策略类          ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);         //生成分表名         String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);         // 获取sql         String sql = boundSql.getSql();         // 完成表名替换         metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));     }      /**      * @param invocation      * @author: 程序猿阿星      * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果      * @date: 2021/5/9      * @return: org.apache.ibatis.reflection.MetaObject      */     private MetaObject getMetaObject(Invocation invocation) {         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();         // MetaObject是mybatis里面提供的一个工具类,类似反射的效果         MetaObject metaObject = MetaObject.forObject(statementHandler,                 SystemMetaObject.DEFAULT_OBJECT_FACTORY,                 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,                 defaultReflectorFactory         );          return metaObject;     }      /**      * @author: 程序猿阿星      * @description: 获取分表注解      * @param method      * @param mappedStatement      * @date: 2021/5/9      * @return: com.xing.shard.interceptor.TableShard      */     private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {         String id = mappedStatement.getId();         //获取Class         final String className = id.substring(0, id.lastIndexOf("."));         //分表注解         TableShard tableShard = null;         //获取Mapper执行方法的TableShard注解         tableShard = method.getAnnotation(TableShard.class);         //如果方法没有设置注解,从Mapper接口上面获取TableShard注解         if (tableShard == null) {             // 获取TableShard注解             tableShard = Class.forName(className).getAnnotation(TableShard.class);         }         return tableShard;     }  }

到了这里,其实分表功能就已经完成了,我们只需要把分表策略抽象接口、分表注解、分表拦截器抽成一个通用jar包,需要使用的项目引入这个jar,然后注册分表拦截器,自己根据业务需求实现分表策略,在给对应的Mpaaer加上分表注解就好了。

实践跑起来

这里阿星单独写了一套demo,场景是有两个分表策略,表也提前建立好了

  • 根据id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根据日期分表

    • tb_log_date_202105

    • tb_log_date_202106

预警:后面都是代码实操环节,请各位读者大大耐心看完(非Java开发除外)。

TableShardStrategy定义

/**  * @Author wx  * @Description 分表策略日期  * @Date 2021/5/9  */ @Component public class TableShardStrategyDate implements ITableShardStrategy {      private static final String DATE_PATTERN = "yyyyMM";      @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);         } else {             return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());         }     } }    **  * @Author 程序猿阿星  * @Description 分表策略id  * @Date 2021/5/9  */ @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //可以加入本地缓存优化         return tableNamePrefix + "_" + (id % 2);     } }

Mapper定义

Mapper接口

/**  * @Author 程序猿阿星  * @Description  * @Date 2021/5/8  */ @TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class) public interface LogDateMapper {      /**      * 查询列表-根据日期分表      */     List queryList();      /**      * 单插入-根据日期分表      */     void  save(LogDate logDate);  }   -------------------------------------------------------------------------------------------------   /**  * @Author 程序猿阿星  * @Description  * @Date 2021/5/8  */ @TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class) public interface LogIdMapper {      /**      * 根据id查询-根据id分片      */     LogId queryOne(@Param("id") long id);      /**      * 单插入-根据id分片      */     void save(LogId logId);   }

Mapper.xml

            //对应LogDateMapper#queryList函数               //对应LogDateMapper#save函数              insert into tb_log_date(id, comment,create_date)         values (#{id}, #{comment},#{createDate})        -------------------------------------------------------------------------------------------------              //对应LogIdMapper#queryOne函数               //对应save函数              insert into tb_log_id(id, comment,create_date)         values (#{id}, #{comment},#{createDate})       

执行下单元测试

日期分表单元测试执行

@Test   void test() {       LogDate logDate = new LogDate();       logDate.setId(snowflake.nextId());       logDate.setComment("测试内容");       logDate.setCreateDate(new Date());       //插入       logDateMapper.save(logDate);       //查询       List logDates = logDateMapper.queryList();       System.out.println(JSONUtil.toJsonPrettyStr(logDates));   }

输出结果

id分表单元测试执行

@Test void test() {     LogId logId = new LogId();     long id = snowflake.nextId();     logId.setId(id);     logId.setComment("测试");     logId.setCreateDate(new Date());     //插入     logIdMapper.save(logId);     //查询     LogId logIdObject = logIdMapper.queryOne(id);     System.out.println(JSONUtil.toJsonPrettyStr(logIdObject)); }

输出结果

小结一下

本文可以当做对Mybatis进阶的使用教程,通过Mybatis拦截器实现分表的功能,满足基本的业务需求,虽然比较简陋,但是Mybatis这种扩展机制与设计值得学习思考。

到此,关于"用Mybatis手写一个分表插件"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

分表 策略 注解 函数 程序 字段 业务 参数 逻辑 拦截器 插件 代码 接口 日期 问题 测试 方法 类型 学习 主体 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 科技创新与互联网经济 互联网最新的科技有限公司 网络科技有限互联网 中国 科技 互联网 万易互联网科技有限公司 凡科互联网科技 中国互联网有什么科技含量 互联网还是高科技吗 电子科技大学互联网创业大赛 工业互联网等科技实现智能化赋能 山东星卡互联网科技有限公司官网 互联网科技资金流入 工业互联网科技创业 郑州欣桑互联网科技有限公司 上海乐五互联网科技有限公司 平安好医生打造互联网科技医疗 上海旺乐尼互联网科技有限公司 宁波允成互联网科技有限公司 互联网科技创业有什么好项目 互联网 科技节活动 微视频大赛 礼当家互联网科技 奔汉互联网科技 长亮科技 互联网金融 以后互联网后是啥科技 港股科技和互联网 和信科技工业互联网 开一家科技互联网公司需要多少钱 元气玛特互联网科技公司是外包吗 江苏互联网智能科技软件 互联网科技博主张洲
0