• 一个完整的分表插件流程


    分表查询的思路很简单,就是在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
  • 相关阅读:
    清北学堂模拟赛d1t5 拍照(photo)
    清北学堂模拟赛d1t4 一道图论好题(graph)
    清北学堂模拟赛d1t3 听音乐(music)
    Android(java)学习笔记167:横竖屏切换时Activity的生命周期
    Android(java)学习笔记166:上下文的区分
    Android(java)学习笔记165:开发一个多界面的应用程序之不同界面间互相传递数据(短信助手案例的优化:请求码和结果码)
    Android(java)学习笔记164:开发一个多界面的应用程序之不同界面间互相传递数据(短信助手案例)
    Android(java)学习笔记163:开发一个多界面的应用程序之界面间数据传递
    Android(java)学习笔记162:开发一个多界面的应用程序之两种意图
    Java基础知识强化19:Java中switch分支语句
  • 原文地址:https://www.cnblogs.com/NoYone/p/9479455.html
Copyright © 2020-2023  润新知