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

<configuration>
<plugins>
<plugin interceptor="com.selicoco.sango.common.database.paginator.interceptor.ShardTableInterceptor">
</plugin>
</plugins>
</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); 最终真正的执行了语句。

所以拦截器的注解内容:

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

3.3 应该拦截什么样的对象

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

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

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

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

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

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

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

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

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

不说话先看图:

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

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

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

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

3.5 真正实现分表查询语句

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

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

3.6 交还被截断的Mybatis执行流

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

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

4 实现源码

4.1 配置文件

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

4.2 分表配置注解

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

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

4.3 分表实现

分表具体实现

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class ShardTableInterceptor implements Interceptor {
private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class);
private static final String tag = ShardTableInterceptor.class.getName(); @Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
String sqlId = mappedStatement.getId(); String className = sqlId.substring(0, sqlId.lastIndexOf("."));
Class<?> classObj = Class.forName(className); TableSeg tableSeg = classObj.getAnnotation(TableSeg.class);
if(null == tableSeg){
//不需要分表,直接传递给下一个拦截器处理
return invocation.proceed();
}
 
//根据配置获取分表字段,生成分表SQL
String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql);
String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth); if (newSql != null) {
logger.debug(tag, "分表后SQL =====>" + newSql);
metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
} // 传递给下一个拦截器处理
return invocation.proceed();
} @Override
public Object plugin(Object target) {
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
} @Override
public void setProperties(Properties properties) {
logger.info("scribeDbNames:" + properties.getProperty("scribeDbNames"));
} //根据配置获取分表的表名后缀
private String genShardByValue(MetaObject metaStatementHandler,MappedStatement mappedStatement, TableSeg tableSeg, BoundSql boundSql) {
String accountMonth = null;
Map<String, Object> additionalParameters = (Map<String, Object>) metaStatementHandler.getValue("delegate.boundSql.additionalParameters"); if (null != additionalParameters.get(tableSeg.shardBy())) {
accountMonth = boundSql.getAdditionalParameter(tableSeg.shardBy()).toString();
} else {
Configuration configuration = mappedStatement.getConfiguration();
String showSql = showSql(configuration,boundSql);
accountMonth = getShardByValue(showSql,tableSeg);
}
return accountMonth;
} //根据配置获取分表参数值
public static String getShardByValue(String showSql,TableSeg tableSeg) {
final String conditionWhere = "where";
String accountMonth = null ;
if(StringUtils.isBlank(showSql)){
return null;
}else{
String[] sqlSplit = showSql.toLowerCase().split(conditionWhere);
if(sqlSplit.length>1 && sqlSplit[1].contains(tableSeg.shardByTable())){
accountMonth = sqlSplit[1].replace(" ","").split(tableSeg.shardByTable())[1].substring(2,8);
}
}
return accountMonth;
} //组装查询语句参数
public static String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", getParameterValue(parameterObject)); } else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
}
}
}
}else{
return null;
}
return sql;
} private static String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}
}
return value;
} }
终于把它补完了~~~ 项目在线运行都几年了~~~ 

基于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. Flutter-ListView

    return Container( child: ListView( children: <Widget>[ Column( children: <Widget>[ Conta ...

  2. 爬虫技术:cookies池的维护

    一:为什么要维护cookie 1.登录才能爬取内容 2.爬取频繁会被封号. 3.需要维护多个账号的cookie,实现大规模抓取 二:cookies的要求 1.自动登录更新 2.定期筛选验证 3.提供外 ...

  3. MySQL查看表索引

    mysql> show index from tblname; mysql> show keys from tblname; · Table 表的名称. · Non_unique 如果索引 ...

  4. Python中的"Special Method"

    The first thing to know about special methods is that they are meant to be called by the Python inte ...

  5. ASP.NET MVC中的捆绑和压缩技术

    概述 在众多Web性能优化的建议中有两条: 减少Http请求数量:大多数的浏览器同时处理向网站处理6个请求(参见下图),多余的请求会被浏览器要求排队等待,如果我们减少这些请求数,其他的请求等待的时间将 ...

  6. HTTP协议-get请求与post请求的区别

    区别: 参数:get通过url进行传递:post放在request body中 长度:get请求在url的长度是有限制的:而post没有(其实这个限制是来自浏览器和web服务器对url的限制,并不是h ...

  7. hdu 1007 Quoit Design (经典分治 求最近点对)

    Problem Description Have you ever played quoit in a playground? Quoit is a game in which flat rings ...

  8. linux 正则表达式与实践

    正则表达式基础 准备 (1)alias grep='grep --color=auto' 易于显示 (2)LC_ALL=C,字符集,设置环境变量,字符顺序 基础正则 1)^word  匹配以Word开 ...

  9. Ant Design Pro (中后台系统)教程

    一.概念:https://pro.ant.design/docs/getting-started-cn(官方网站) 1.Ant Design Pro 是什么:  https://www.cnblogs ...

  10. iOS Android中 h5键盘遮挡输入框的问题和解决方案

    问题发现:在 Android 部分机型 和 iOS部分系统下 键盘会出现遮挡输入框的情况(壳内).问题解决: Android 经过测试,Android 的6.0版本以上均会出现改问题,归根到底是之前的 ...