1、拦截器简介

MyBatis提供了一种插件(plugin)的功能,但其实这是拦截器功能。基于这个拦截器我们可以选择在这些被拦截的方法执行前后加上某些逻辑或者在执行这些被拦截的方法时执行自己的逻辑。

这点跟spring的拦截器是基本一致的。它的设计初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。

拦截器的使用中,分页插件应该是使用得最多的了。分表的实现也差不多类似。

2、拦截的方法调用

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

总体概括为:

  1. 拦截执行器的方法
  2. 拦截参数的处理
  3. 拦截结果集的处理
  4. 拦截Sql语法构建的处理

我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。

这4各方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。undefine

3、Interceptor接口

了解到了拦截器能够拦截的方法调用,就需要看看拦截接口是如何实现的了。

package org.apache.ibatis.plugin; 
    
import java.util.Properties; 
    
public interface Interceptor { 
    
  Object intercept(Invocation invocation) throws Throwable; 
    
  Object plugin(Object target); 
    
  void setProperties(Properties properties); 
    

接口中一共定义有三个方法,intercept、plugin 、setProperties。

  • intercept方法就是要进行拦截的时候要执行的方法。
  • setProperties方法是用于在Mybatis配置文件中指定一些属性的。
  • plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。

分表实现

1、大体思路

基于业务来看,我想要 按月 分表,因此数据库表里增加了一个string类型字段 account_month 来记录月份,分表字段就使用account_month。

分表表名:表名_年月   例如明细表:ebs_date_detail_201607。

分表是一月一张表,分表的建立就是默认建立了12个分表,如果超出了,后续再手工添加吧。也可以写个脚本每月底创建下一个月的表,但是我觉得没啥必要。就算哪天忘记添加了,代码逻辑的异常处理流程里面也能够保证我的数据不丢失,启动一下异常数据处理也就妥妥的了。

在sql语句里面会要求带上分表字段,通过分表字段计算得到分表的表名,然后替换掉原来的sql,直接将数据路由到指定的分表就行了。

听起来好像很简单的样子,那么就这么出发吧。

2、问题目录

分表开始之前的问题:

  1. Mybatis如何找到我们新增的拦截服务。
  2. 自定义的拦截服务应该在什么时间拦截查询动作。即什么时间截断Mybatis执行流。
  3. 自定义的拦截服务应该拦截什么样的对象。不能拦截什么样的对象。
  4. 自定义的拦截服务拦截的对象应该具有什么动作才能被拦截。
  5. 自定义的拦截服务如何获取上下文中传入的参数信息。
  6. 如何把简单查询,神不知鬼不觉的,无侵入性的替换为分表查询语句。
  7. 最后,拦截器应该如何交还被截断的Mybatis执行流。

带着这些问题,我们来看看我们自定义的拦截服务是如何实现的。

3、逐步实现

3.1 Mybatis如何找到我们新增的拦截服务

对于拦截器Mybatis为我们提供了一个Interceptor接口,前面有提到,通过实现该接口就可以定义我们自己的拦截器。自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,拦截器需要注册到mybatis-config配置文件中。

通过在Mybatis配置文件中plugins元素下的plugin元素来进行。一个plugin对应着一个拦截器,在plugin元素下面我们可以指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。

配置文件:mybatis-config.xml

  1. <configuration>
  2. <plugins>
  3. <plugin interceptor="com.selicoco.sango.common.database.paginator.interceptor.ShardTableInterceptor">
  4. </plugin>
  5. </plugins>
  6. </configuration>

3.2 什么时间截断Mybatis执行流

Mybatis允许我们能够进行切入的点:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  2. ParameterHandler (getParameterObject, setParameters)

  3. ResultSetHandler (handleResultSets, handleOutputParameters)

  4. StatementHandler (prepare, parameterize, batch, update, query)

因为我是想要通过替换原来SQL中的表名来实现分表,包括查询,新增,删除等操作,所以拦截的合理时机选在StatementHandler中prepare。

执行流在PreparedStatementHandler.instantiateStatement()方法中 return connection.prepareStatement(sql); 最终真正的执行了语句。

所以拦截器的注解内容:

  1. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) 

3.3 应该拦截什么样的对象

并不是所有的表都进行了分表,也不是所有的表都需要拦截处理。所以我们要根据某些配置来确定哪些需要被处理。

这里主要使用注解的方式,设置了对应的参数。

  1. @Target({ ElementType.TYPE, ElementType.METHOD })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Inherited
  5. public @interface TableSeg {
  6. //表名
  7. public String tableName();
  8. // 分表方式,取模,如%5:表示取5余数,
  9. // 按时间,如MONTH:表示按月分表
  10. // 如果不设置,直接根据shardBy值分表
  11. public String shardType();
  12. //根据什么字段分表 ,多个字段用数学表达表示,如a+b a-b
  13. public String shardBy();
  14.  
  15. // 根据什么字段分表,多个字段用数学表达表示,如a+b a-b
  16. public String shardByTable();
  17. }

注解完成后,在mapper上去配置。如果是自定义的查询语句和返回,没有对应的mapper文件,那么在对应的dao 上进行配置就可以了。

  1. @TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month")
  2. public interface EbsDataDetailMapper {}
  1. @Repository
  2. @TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month")
  3. public class EbsDataDetailDao {}

3.4 如何获取上下文中传入的参数

关于这个问题,我觉得我有很大的发言权,真的是摸着石头过来的。

天真的以为,只要拿到要执行之前已经组装好的语句,然后用我的分表表名替换一下原表名就可以了。当然其实也差不多就是这样子的。不过实际情况还是有点坎坷的。

首先,如何拿到执行前已经组装好的语句。分两种情况来说,查询和更新。

不说话先看图:

新增数据的时候,我们从boundSql里面的additionalParameters 里面能轻松拿到注解上面 shardBy="accountMonth"所对应的参数值。然后根据参数来生成分表语句,一切顺利。

如此简单,觉得自己好机智。开心的去码后面的代码了,等到单测的时候执行查询,然后就报错啦。只能Debug看看。

没有想到,都是mybatis的动态sql,结果参数方式竟然不同,想来也只能自己去取参数了。参数在哪里?看图

具体的就看后面实现代码吧,反正就是通过两种方式取到我们要的分表字段的参数值,这样才能求得分表表名。

3.5 真正实现分表查询语句

拦截器主要的作用是读取配置,根据配置的切分策略和字段,来切分表,然后替换原执行的SQL,从而实现自动切分。

  1. String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql);
  2. String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth);
  3.  
  4. if (newSql != null) {
  5. logger.debug(tag, "分表后SQL =====>" + newSql);
  6. metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
  7. }

3.6 交还被截断的Mybatis执行流

把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis了

  1. // 传递给下一个拦截器处理
  2. return invocation.proceed();

4 实现源码

4.1 配置文件

见本文:  3.1 Mybatis如何找到我们新增的拦截服务 -- mybatis-config.xml

4.2 分表配置注解

分表注解定义、mapper注解配置、DAO注解配置

见本文: 3.3 应该拦截什么样的对象

4.3 分表实现

分表具体实现

  1. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
  2. public class ShardTableInterceptor implements Interceptor {
  3. private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class);
  4. private static final String tag = ShardTableInterceptor.class.getName();
  5.  
  6. @Override
  7. public Object intercept(Invocation invocation) throws Throwable {
  8. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  9. MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
  10. MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
  11. BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
  12. String sqlId = mappedStatement.getId();
  13.  
  14. String className = sqlId.substring(0, sqlId.lastIndexOf("."));
  15. Class<?> classObj = Class.forName(className);
  16.  
  17. TableSeg tableSeg = classObj.getAnnotation(TableSeg.class);
  18. if(null == tableSeg){
  19. //不需要分表,直接传递给下一个拦截器处理
  20. return invocation.proceed();
  21. }
  22.  
  23. //根据配置获取分表字段,生成分表SQL
  24. String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql);
  25. String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth);
  26.  
  27. if (newSql != null) {
  28. logger.debug(tag, "分表后SQL =====>" + newSql);
  29. metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
  30. }
  31.  
  32. // 传递给下一个拦截器处理
  33. return invocation.proceed();
  34. }
  35.  
  36. @Override
  37. public Object plugin(Object target) {
  38. // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
  39. if (target instanceof StatementHandler) {
  40. return Plugin.wrap(target, this);
  41. } else {
  42. return target;
  43. }
  44. }
  45.  
  46. @Override
  47. public void setProperties(Properties properties) {
  48. logger.info("scribeDbNames:" + properties.getProperty("scribeDbNames"));
  49. }
  50.  
  51. //根据配置获取分表的表名后缀
  52. private String genShardByValue(MetaObject metaStatementHandler,MappedStatement mappedStatement, TableSeg tableSeg, BoundSql boundSql) {
  53. String accountMonth = null;
  54. Map<String, Object> additionalParameters = (Map<String, Object>) metaStatementHandler.getValue("delegate.boundSql.additionalParameters");
  55.  
  56. if (null != additionalParameters.get(tableSeg.shardBy())) {
  57. accountMonth = boundSql.getAdditionalParameter(tableSeg.shardBy()).toString();
  58. } else {
  59. Configuration configuration = mappedStatement.getConfiguration();
  60. String showSql = showSql(configuration,boundSql);
  61. accountMonth = getShardByValue(showSql,tableSeg);
  62. }
  63. return accountMonth;
  64. }
  65.  
  66. //根据配置获取分表参数值
  67. public static String getShardByValue(String showSql,TableSeg tableSeg) {
  68. final String conditionWhere = "where";
  69. String accountMonth = null ;
  70. if(StringUtils.isBlank(showSql)){
  71. return null;
  72. }else{
  73. String[] sqlSplit = showSql.toLowerCase().split(conditionWhere);
  74. if(sqlSplit.length>1 && sqlSplit[1].contains(tableSeg.shardByTable())){
  75. accountMonth = sqlSplit[1].replace(" ","").split(tableSeg.shardByTable())[1].substring(2,8);
  76. }
  77. }
  78. return accountMonth;
  79. }
  80.  
  81. //组装查询语句参数
  82. public static String showSql(Configuration configuration, BoundSql boundSql) {
  83. Object parameterObject = boundSql.getParameterObject();
  84. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  85. String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
  86. if (parameterMappings.size() > 0 && parameterObject != null) {
  87. TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  88. if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  89. sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
  90.  
  91. } else {
  92. MetaObject metaObject = configuration.newMetaObject(parameterObject);
  93. for (ParameterMapping parameterMapping : parameterMappings) {
  94. String propertyName = parameterMapping.getProperty();
  95. if (metaObject.hasGetter(propertyName)) {
  96. Object obj = metaObject.getValue(propertyName);
  97. sql = sql.replaceFirst("\\?", getParameterValue(obj));
  98. } else if (boundSql.hasAdditionalParameter(propertyName)) {
  99. Object obj = boundSql.getAdditionalParameter(propertyName);
  100. sql = sql.replaceFirst("\\?", getParameterValue(obj));
  101. }
  102. }
  103. }
  104. }else{
  105. return null;
  106. }
  107. return sql;
  108. }
  109.  
  110. private static String getParameterValue(Object obj) {
  111. String value = null;
  112. if (obj instanceof String) {
  113. value = "'" + obj.toString() + "'";
  114. } else if (obj instanceof Date) {
  115. DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
  116. value = "'" + formatter.format(new Date()) + "'";
  117. } else {
  118. if (obj != null) {
  119. value = obj.toString();
  120. } else {
  121. value = "";
  122. }
  123. }
  124. return value;
  125. }
  126.  
  127. }
终于把它补完了~~~ 项目在线运行都几年了~~~ 

基于mybatis拦截器分表实现的更多相关文章

  1. 数据权限管理中心 - 基于mybatis拦截器实现

    数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...

  2. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  3. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  4. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  5. Mybatis拦截器介绍及分页插件

    1.1    目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2     前言 拦截器的一 ...

  6. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

  7. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  8. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

  9. 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离

    前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...

随机推荐

  1. RBAC用户权限管理数据库设计的图文详解

    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成“用户-角色- ...

  2. MySQL DDL Demo

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11606833.html DDL Demo CREATE TABLE `user` ( `id` ) u ...

  3. php strncasecmp()函数 语法

    php strncasecmp()函数 语法 作用:比较字符串前n个字符,不区分大小写直线电机 语法:strncasecmp(string1,string2,length) 参数: 参数 描述 str ...

  4. PHP 利用 curl 发送 post get del put patch 请求

    因为需要在 php 开发中对接其它接口需要用 php curl 去对接其它接口 我把他们封装成函数 希望能对大家有所帮助. 这里面是封装好的会自动把 data 进行转成 json 格式,同时解码成 p ...

  5. DM9000网卡驱动深度分析

    一.dm9000_porbe函数分析 不同于u-boot代码,tq2440中的DM9000更加复杂,需要分析的点也很多: /* * Search DM9000 board, allocate spac ...

  6. linux 下启动tomcat 时没有执行权限

    原因: 没有权限 解决 : chmod 777 *.sh Linux下启动tomcat

  7. Python中的时间模块和日期模块

    Python 日期和时间 Python 程序能用很多方式处理日期和时间,转换日期格式是一个常见的功能. Python 提供了一个 time 和 calendar 模块可以用于格式化日期和时间. 时间间 ...

  8. LUOGU P4783 【模板】矩阵求逆(高斯消元)

    传送门 解题思路 用高斯消元对矩阵求逆,设\(A*B=C\),\(C\)为单位矩阵,则\(B\)为\(A\)的逆矩阵.做法是把\(B\)先设成单位矩阵,然后对\(A\)做高斯消元的过程,对\(B\)进 ...

  9. [杂题]:C/c(二分答案)

    题目传送门(内部题54) 输入格式 第一行一个整数表示$n$.第二行$n$个整数表示初始序列.(这行原题没有,是我加的)接下来$2n$行每行两个整数,分别表示$X_i,Y_i$.数据保证至少存在一种方 ...

  10. Git-学习开源代码的技巧

    从最初提交开始学习每次提交的代码 https://stackoverflow.com/questions/5630110/how-to-read-source-code-using-git 很久以前就 ...