首页 > 文章列表 > 怎么使用springboot+mybatis拦截器实现水平分表

怎么使用springboot+mybatis拦截器实现水平分表

mybatis springboot
252 2023-05-20

怎么使用springboot+mybatis拦截器实现水平分表

MyBatis 允许使用插件来拦截的方法

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

总体概括为:

  • 拦截执行器的方法

  • 拦截参数的处理

  • 拦截结果集的处理

  • 拦截Sql语法构建的处理

这4各方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。

Interceptor接口

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

//intercept方法就是要进行拦截的时候要执行的方法。

  Object intercept(Invocation invocation) throws Throwable;

//plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。

  Object plugin(Object target);

//setProperties方法是用于在Mybatis配置文件中指定一些属性的。

  void setProperties(Properties properties);



}

分表实现

1、大体思路

分表的表结构已经预设完毕,所以现在我们只需要在进行增删改查的时候直接一次锁定目标表,然后替换目标sql。

2、逐步实现

Mybatis如何找到我们新增的拦截服务

对于拦截器Mybatis为我们提供了一个Interceptor接口,前面有提到,通过实现该接口就可以定义我们自己的拦截器。自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,利用springboot把自定义拦截器注入。

package com.shinemo.insurance.common.config;

 

import org.apache.ibatis.plugin.Interceptor;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

 

@Configuration

public class TableShardConfig {



    /**

     * 注册插件

     */

    @Bean

    public Interceptor tableShardInterceptor() {

        return new TableShardInterceptor();

    }



}
应该拦截什么样的对象

因为拦截器是全局拦截的,我们只需要拦截我们需要拦截的mapper,故需要用注解进行标识

package com.shinemo.insurance.common.annotation;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



@Target(value = { ElementType.TYPE, ElementType.METHOD })

@Retention(RetentionPolicy.RUNTIME)

public @interface TableShard {



    // 表前缀名

    String tableNamePrefix();



    // 值

    String value() default "";



    // 是否是字段名,如果是需要解析请求参数改字段名的值(默认否)

    boolean fieldFlag() default false;



}

我们只需要把这个注解标识在我们要拦截的mapper上

@Mapper

@TableShard(tableNamePrefix = "t_insurance_video_people_", value = "deviceId", fieldFlag = true)

public interface InsuranceVideoPeopleMapper {

 

//VideoPeople对象中包含deviceId字段

  int insert(VideoPeople videoPeople);

}
实现自定义拦截器
package com.shinemo.insurance.common.config;



import java.lang.reflect.Field;

import java.lang.reflect.Method;

import java.sql.Connection;

import java.util.Map;



import com.shinemo.insurance.common.annotation.TableShard;

import com.shinemo.insurance.common.util.HashUtil;



import org.apache.ibatis.binding.MapperMethod;

import org.apache.ibatis.executor.statement.StatementHandler;

import org.apache.ibatis.mapping.BoundSql;

import org.apache.ibatis.mapping.MappedStatement;

import org.apache.ibatis.plugin.Interceptor;

import org.apache.ibatis.plugin.Intercepts;

import org.apache.ibatis.plugin.Invocation;

import org.apache.ibatis.plugin.Plugin;

import org.apache.ibatis.plugin.Signature;

import org.apache.ibatis.reflection.DefaultReflectorFactory;

import org.apache.ibatis.reflection.MetaObject;

import org.apache.ibatis.reflection.ReflectorFactory;

import org.apache.ibatis.reflection.SystemMetaObject;

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

                                                                                    Integer.class }) })

public class TableShardInterceptor implements Interceptor {

    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Override

    public Object intercept(Invocation invocation) throws Throwable {



        // MetaObject是mybatis里面提供的一个工具类,类似反射的效果

        MetaObject metaObject = getMetaObject(invocation);

        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");

        MappedStatement mappedStatement = (MappedStatement) metaObject

            .getValue("delegate.mappedStatement");



        // 获取Mapper执行方法

        Method method = invocation.getMethod();



        // 获取分表注解

        TableShard tableShard = getTableShard(method, mappedStatement);



        // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑

        if (tableShard == null) {

            return invocation.proceed();

        }



        //获取值,此值就是拿的注解上value值,注解上value设定的值,并在传入对象中获取,根据业务可以选择适当的值即可,我选取此值的目的是同一台设备的值存入一张表中,有hash冲突的值也存在一张表中

        String value = tableShard.value();

        //value是否字段名,如果是,需要解析请求参数字段名的值

        boolean fieldFlag = tableShard.fieldFlag();



        if (fieldFlag) {

            //获取请求参数

            Object parameterObject = boundSql.getParameterObject();



            if (parameterObject instanceof MapperMethod.ParamMap) {

                // ParamMap类型逻辑处理

                MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;

                // 根据字段名获取参数值

                Object valueObject = parameterMap.get(value);

                if (valueObject == null) {

                    throw new RuntimeException(String.format("入参字段%s无匹配", value));

                }

                //替换sql

                replaceSql(tableShard, valueObject, metaObject, boundSql);



            } else {

                // 单参数逻辑



                //如果是基础类型抛出异常

                if (isBaseType(parameterObject)) {

                    throw new RuntimeException("单参数非法,请使用@Param注解");

                }



                if (parameterObject instanceof Map) {

                    Map<String, Object> parameterMap = (Map<String, Object>) parameterObject;

                    Object valueObject = parameterMap.get(value);

                    //替换sql

                    replaceSql(tableShard, valueObject, metaObject, boundSql);

                } else {

                    //非基础类型对象

                    Class<?> parameterObjectClass = parameterObject.getClass();

                    Field declaredField = parameterObjectClass.getDeclaredField(value);

                    declaredField.setAccessible(true);

                    Object valueObject = declaredField.get(parameterObject);

                    //替换sql

                    replaceSql(tableShard, valueObject, metaObject, boundSql);

                }

            }



        } else {//无需处理parameterField

            //替换sql

            replaceSql(tableShard, value, metaObject, boundSql);

        }

        //把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis下一个拦截器处理

        return invocation.proceed();

    }



    /**

     * @description:

     * @param target

     * @return: Object

     */

    @Override

    public Object plugin(Object target) {

        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数

        if (target instanceof StatementHandler) {

            return Plugin.wrap(target, this);

        } else {

            return target;

        }

    }



    /**

     * @description: 基本数据类型验证,true是,false否

     * @param object

     * @return: boolean

     */

    private boolean isBaseType(Object object) {

        if (object.getClass().isPrimitive() || object instanceof String || object instanceof Integer

            || object instanceof Double || object instanceof Float || object instanceof Long

            || object instanceof Boolean || object instanceof Byte || object instanceof Short) {

            return true;

        } else {

            return false;

        }

    }

    /**

     * @description: 替换sql

     * @param tableShard 分表注解

     * @param value      值

     * @param metaObject mybatis反射对象

     * @param boundSql   sql信息对象

     * @return: void

     */

    private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject,

                            BoundSql boundSql) {

        String tableNamePrefix = tableShard.tableNamePrefix();

        //        // 获取策略class

        //        Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();

        //        // 从spring ioc容器获取策略类

        //        ITableShardStrategy tableShardStrategy = SpringBeanUtil.getBean(strategyClazz);

        // 生成分表名

        String shardTableName = generateTableName(tableNamePrefix, (String) value);

        // 获取sql

        String sql = boundSql.getSql();

        // 完成表名替换

        metaObject.setValue("delegate.boundSql.sql",

            sql.replaceAll(tableNamePrefix, shardTableName));

    }



    /**

     * 生成表名

     *

     * @param tableNamePrefix 表名前缀

     * @param value           价值

     * @return {@link String}

     */

    private String generateTableName(String tableNamePrefix, String value) {



//我们分了1024张表

        int prime = 1024;

//hash取模运算过后,锁定目标表

        int rotatingHash = HashUtil.rotatingHash(value, prime);

        return tableNamePrefix + rotatingHash;

    }

    /**

     * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果

     * @param invocation

     * @return: MetaObject

     */

    private MetaObject getMetaObject(Invocation invocation) {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        // MetaObject是mybatis里面提供的一个工具类,类似反射的效果

        MetaObject metaObject = MetaObject.forObject(statementHandler,

            SystemMetaObject.DEFAULT_OBJECT_FACTORY,

            SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);



        return metaObject;

    }

    /**

     * @description: 获取分表注解

     * @param method

     * @param mappedStatement

     * @return: TableShard

     */

    private TableShard getTableShard(Method method,

                                     MappedStatement mappedStatement) throws ClassNotFoundException {

        String id = mappedStatement.getId();

        // 获取Class

        final String className = id.substring(0, id.lastIndexOf("."));

        // 分表注解

        TableShard tableShard = null;

        // 获取Mapper执行方法的TableShard注解

        tableShard = method.getAnnotation(TableShard.class);

        // 如果方法没有设置注解,从Mapper接口上面获取TableShard注解

        if (tableShard == null) {

            // 获取TableShard注解

            tableShard = Class.forName(className).getAnnotation(TableShard.class);

        }

        return tableShard;

    }

}