Mybatis拦截器实现水平分表
Mybatis插件(plugins)
Mybatis允许在已映射的语句执行过程中某一点进行拦截。Mybatis允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
分别拦截以下方法调用:
- 拦截执行器的方法
- 拦截参数的处理
- 拦截结果集的处理
- 拦截Sql语法构建的处理
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口:
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import java.util.Properties;
@Intercepts({@Signature(
type= Executor.class,method = "update",args = {MappedStatement.class,Object.class})
})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
@Override
public Object plugin(Object o) {
return null;
}
@Override
public void setProperties(Properties properties) {
}
}
上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象。
水平分表实现
自定义插件
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
SpringBoot中只需要使用@Component注解注册为bean即可
选择拦截方法
实现分表主要是通过在sql构建时,对表名进行替换,所以选择拦截StatementHandler
注解为:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class })
})
定义分区表、分表字段
使用Mybatis实现分表我们期望分表灵活,即可以选择要分区的表,分区表分表字段,甚至指定哪些方法需要分表
自定义注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface TablesPartition {
// 是否分表
boolean split() default true;
TablePartition[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface TablePartition {
// 表名
String value() default "";
// 字段
String field() default "";
}
在Mapper接口/方法上定义注解
@TablesPartition({
@TablePartition(value = "table_0", field = "field_0"),
@TablePartition(value = "table_1", field = "field_0")
})
public interface NormalMapper {
@TablesPartition({
@TablePartition(value = "table_0", field = "field_0")
})
List<NormalEntity> selectAll();
}
分表核心源码实现
源码实现:
@Slf4j
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class })
})
public class PartitionInterceptor implements Interceptor {
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,REFLECTOR_FACTORY);
Object parameterObject = metaStatementHandler.getValue("delegate.boundSql.parameterObject");
partitionTable(metaStatementHandler,parameterObject);
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) {
}
private void partitionTable(MetaObject metaStatementHandler, Object param ) throws Exception {
String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
if (StringUtils.isNotBlank(originalSql)) {
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
Class<?> clazz = Class.forName(className);
Method method = findMethod(clazz.getDeclaredMethods(), methodName);
// 根据配置自动生成分表SQL,不配置查主表
TablesPartition tablesPartition = null;
if (method != null) {
tablesPartition = method.getAnnotation(TablesPartition.class);
}
if (tablesPartition == null) {
tablesPartition = clazz.getAnnotation(TablesPartition.class);
}
if (tablesPartition != null && tablesPartition.split()) {
TablePartition[] tablePartitionList = tablesPartition.value();
String convertedSql = originalSql;
for (TablePartition tablePartition:tablePartitionList) {
StringBuilder stringBuilder = new StringBuilder(tablePartition.value());
String resort = "";
if (param instanceof Map) {
resort = (String)((Map) param).get(tablePartition.field());
} else if (param instanceof String) {
resort = (String)param;
}
if (!StringUtils.isEmpty(resort)) {
stringBuilder.append("_");
stringBuilder.append(resort);
}
// 替换表名前先把包含表名的字段名*_替换为"thisIsSpecialColumn"
convertedSql = convertedSql.replaceAll("(?i)" + tablePartition.value()+"_", "thisIsSpecialColumn");
// 替换表名,不区分大小写
convertedSql = convertedSql.replaceAll("(?i)" + tablePartition.value(), stringBuilder.toString());
// 替换表名完成把"thisIsSpecialColumn"替换回字段名*_
convertedSql = convertedSql.replaceAll("thisIsSpecialColumn", tablePartition.value()+"_");
}
log.debug("分表后的SQL:\n" + convertedSql);
metaStatementHandler.setValue("delegate.boundSql.sql", convertedSql);
}
}
}
private Method findMethod(Method[] methods, String methodName) {
for (Method method : methods) {
if (method.getName().equals(methodName)) {
return method;
}
}
return null;
}
}