利用mybatis插件开发动态更改sql

1、业务背景需求

目前楼主所在公司线上与预发环境用的是同一套数据库,这样做的目的是在预发环境验收线上的真实情况。但是有几个配置表需要单独在预发环境更改,如果更改了会影响到线上用户使用,所以如何将配置表做环境隔离,在不更改现有代码的情况下;做法就是利用mybatis提供的插件开发机制对底层的4大内置对象进行拦截;那么4大内置对象都有哪些?


2、4大内置对象

  1. Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
  2. StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  3. ParameterHandler:是用来处理SQL参数的。
  4. ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的。

4大内置对象与mybatis执行一条sql的生命周期息息相关,具体执行流程参考另一篇博客


3、具体执行

我们的方案是新增加一套预发配置表(生产环境),比如之前的配置表叫table_config,新增加的表则为table_config_pre,当然数据也要复制过来,然后根据系统运行环境变量来判断,如果是预发环境的话,对4大内置对象中的StatementHandler进行拦截,StatementHandler的prepare方法会对sql进行预编译,在这里我们可以拿到sql,如果是我们要更改的配置表的话,则进行表替换;具体代码如下:

@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ReRoutePlugin implements Interceptor, ApplicationContextAware {

    private static final String PRE = "pre";

    private ApplicationContext context;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 只在预发环境生效
        if(context.getEnvironment().getActiveProfiles()[0].equals(PRE)){
            return invocation.proceed();
        }

        try {
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = statementHandler.getBoundSql();
            String sqlBefore = boundSql.getSql();

            boolean containsChannel = sqlBefore.contains("channel_config");

            if(StringUtils.isNotBlank(sqlBefore) && containsChannel){
                String sqlAfter = sqlBefore.replaceAll("channel_config", "channel_config_pre");
               

                Field sqlField = boundSql.getClass().getDeclaredField("sql");
                ReflectionUtils.setField(sqlField, boundSql, sqlAfter);
                log.info("before sql:{},after sql:{}", sqlBefore, sqlAfter);
            }
        }catch (Exception e){
            log.error("ChannelConfigReRoutePlugin error.", e);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

mybatis原理解析_promisessh的博客-CSDN博客 这样可以在不更改业务代码的情况下 在底层进行偷梁换柱


4、插件开发原理

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
    }
}

4大内置对象执行前都会执行Interceptor中的plugin方法,看下plugin方法

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

而 wrap方法会读取当前所有的插件,并生成代理类,去执行插件中的方法


版权声明:本文为promisessh原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。