1、业务背景需求
目前楼主所在公司线上与预发环境用的是同一套数据库,这样做的目的是在预发环境验收线上的真实情况。但是有几个配置表需要单独在预发环境更改,如果更改了会影响到线上用户使用,所以如何将配置表做环境隔离,在不更改现有代码的情况下;做法就是利用mybatis提供的插件开发机制对底层的4大内置对象进行拦截;那么4大内置对象都有哪些?
2、4大内置对象
- Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
- StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
- ParameterHandler:是用来处理SQL参数的。
- 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版权协议,转载请附上原文出处链接和本声明。