一个完整的分表插件流程

分表查询的思路很简单,就是在sql的运行过程中的某一阶段,拦截下sql,将它“自动”路由到分表中的任意一个

一、Mybatis Interceptor接口使用

  按照思路所说,自然要想办法把运行到某一阶段的sql拦截下来并做更改,那么就需要Interceptor。

  Interceptor可以拦截的方法,官网描述如下:

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

    •   Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    •   ParameterHandler (getParameterObject, setParameters)
    •   ResultSetHandler (handleResultSets, handleOutputParameters)
    •   StatementHandler (prepare, parameterize, batch, update, query)

  这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。

  sql语句是被封装在BoundSql里的,而BoundSql由StatementHandler获取,所以我们拦截StatementHandler的prepare方法(StatementHandler和BoundSql部分源码如下)。

public interface StatementHandler {

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}
public class BoundSql {

  private String sql;

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    ......
  }

  public String getSql() {
    return sql;
  }

}
View Code

  在写插件的时候,我们只需要在插件类上添加注解:@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class })}) 

  注解中属性的意义应该一看便知吧,只要准备写一个Mybatis插件类,就必须添加@Intercepts注解。

  Interceptor接口中方法介绍

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

  ① intercept方法:执行拦截内容。下面的plugin方法触发该方法。

  ② plugin方法:用于给target创建一个jdk的动态代理对象,用于触发intercept方法。这个方法的实现中一般只写一句话Plugin.wrap(target,this),可以看一下这个wrap方法:

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;

  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
     // 当生产的动态代理类运行到super.h.invoke时,调用了intercept方法。
return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } }

  看到了实现了InvocationHandler接口,有invoke方法,就知道这和动态代理(关于动态代理可以看我的另一篇:https://www.cnblogs.com/NoYone/p/8733868.html)有关,然后我们看wrap方法,实际上就是返回了target的动态代理之后的对象。

  ③ setProperties()方法:给自定义的拦截器传递xml配置的属性参数。

二、intercept实现方法

   在intercept方法中最重要的是拿到sql语句,而sql语句是被封装到顶层被代理类里的,所以需要从StatementHandler往“上”遍历获得顶层被代理类。

        // 取出被拦截的对象
        StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStmtHandler = SystemMetaObject.forObject(stmtHandler);
        // 分离代理对象,从而形成多次代理,通过两次循环最原始的被代理类,Mybatis使用的是JDK代理
        while (metaStmtHandler.hasGetter("h")) {
            Object object = metaStmtHandler.getValue("h");
            metaStmtHandler = SystemMetaObject.forObject(object);
        }
 
        // 分离最后一个代理目标类
        while (metaStmtHandler.hasGetter("target")) {
            Object object = metaStmtHandler.getValue("target");
            metaStmtHandler = SystemMetaObject.forObject(object);
        }

        String sql = (String) metaStmtHandler.getValue("delegate.boundSql.sql");
        Object param = metaStmtHandler.getValue("delegate.boundSql.parameterObject");       

  至此,我们已经拿到了sql语句和sql的参数,接下来我们要替换sql中的分表标志,使sql语句路由到对应的表去。

三、分表路由规则的制定

  比方说我们有20张表,什么时候去请求哪一张表,这肯定是需要一定的规则的,根据业务需求自行设计即可。一般情况下就用取模就可以了。

  我们这里设置三个字段:

  1.  symbol 分表标识符,即判断此条sql是否需要分表,若带这个这个标识符,则进入分表逻辑
  2. filedName 分表列,即根据哪一个字段去做分表
  3. splitConut 分表的总个数,有这个数呢,就方便取模,路由找表

  这种字段可以设置在mybatis的配置文件中,由上面介绍过的setProperties方法set进来。示例如下

    <plugins>
        <plugin interceptor="com.jd.fspinvoice.plugin.SplitTablePluginXXX">
            <!--取膜20标号范围0,19 -->
            <property value="20" name="splitCount"/>
            <property value="rid" name="filedName"/>
            <property value="@2" name="symbol"/>
        </plugin>

    </plugins>


            public void setProperties(Properties properties) {
        try {
            splitCount = Integer.valueOf((String) properties.get("splitCount"));
            filedName = (String) properties.get("filedName");
            symbol = (String) properties.get("symbol");
        } catch (Exception e) {
            logger.error("未设置分表数量", e);
            throw new RuntimeException("未设置分表数量");
        }
        this.props = properties;
    }
View Code

  也可以在传入sql语句的参数中,比方说Mapper接口设置接口的是一个Map,那么map里就要set上这个这个filedName即可。

四、完整的Intercept方法

@Override
    public Object intercept(Invocation invocation) throws Throwable {

            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            MetaObject metaStmtHandler = SystemMetaObject.forObject(statementHandler);
            while (metaStmtHandler.hasGetter("h")) {
                Object object = metaStmtHandler.getValue("h");
                metaStmtHandler = SystemMetaObject.forObject(object);
                while (metaStmtHandler.hasGetter("target")) {
                    object = metaStmtHandler.getValue("target");
                    metaStmtHandler = SystemMetaObject.forObject(object);
                }
            }

            String sql = (String) metaStmtHandler.getValue("delegate.boundSql.sql");
            Object param = metaStmtHandler.getValue("delegate.boundSql.parameterObject");
             sql = sql.trim();


            String lowSql = sql.toLowerCase();
            if (lowSql.startsWith("insert") || lowSql.startsWith("update") || lowSql.startsWith("delete")|| lowSql.startsWith("select")) {
                if (lowSql.indexOf(symbol) != -1) {
                    Long filedValue = getBusinessValue(param, filedName,Long.class);
                    if(filedValue == null){
                        throw new RuntimeException("需要路由字段:"+filedName );
                    }
                    long hash = getHashLong(String.valueOf(filedValue));
                    logger.info("此SQL需要进行路由操作。 表坐标:" + hash % splitCount + ", 路由字段:" + filedName+",值:"+filedValue);
                    // 取模操作
                    sql = generateSql(sql, new Long(hash % splitCount).toString(),symbol);
                    metaStmtHandler.setValue("delegate.boundSql.sql", sql);
                } else {
                    // 无@标识不需要分表无需处理
                }
            } else {
                // 不走路由
            }
            return invocation.proceed();
    }
View Code

相关推荐