一个完整的分表插件流程
分表查询的思路很简单,就是在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; } }
在写插件的时候,我们只需要在插件类上添加注解:@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张表,什么时候去请求哪一张表,这肯定是需要一定的规则的,根据业务需求自行设计即可。一般情况下就用取模就可以了。
我们这里设置三个字段:
- symbol 分表标识符,即判断此条sql是否需要分表,若带这个这个标识符,则进入分表逻辑
- filedName 分表列,即根据哪一个字段去做分表
- 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; }
也可以在传入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(); }