MyBatis拦截器修改待执行sql表名
项目中,有时候为了防止表数据过大,要对表中的一部分用不到的历史数据迁移出去(迁移到另外一个表),但是随着业务需要,偶尔需要查询被迁移出去的历史数据。在此通过Mybatis拦截器、自定义注解、获取参数来修改表名
一、自定义注解
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TableNameChange {
/**
* 替换前的表名
*/
String[] originalTableNames();
/**
* 替换后对表名
*/
String[] modifiedTableNames();
}
二、实现Mybatis的Interceptor接口
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
@Slf4j
@Component
public class TableNameChangeInterceptor implements Interceptor {
//使用该方法在拦截中途添加自己想要的自定义操作。
Object intercept(Invocation var1) throws Throwable;
//将插件添加到mybatis操作中。
default Object plugin(Object target) {
//其中这里就是将拦截器和对象包装在一起:详细见下面
return Plugin.wrap(target, this);
}
//读取配置中的属性。
default void setProperties(Properties properties) {}
}
2.1 添加@Intercepts注解
Intercepts是一个Signature类型的数组,通过该注解去判断当前方法是否需要被拦截。
@Signature注解的三个参数
-
type 指定拦截的类的类型,type有4个类型可选
// Mybatis中所有的Mapper语句的执行都是通过Executor进行的。Executor是Mybatis的核心接口。从其定义的接口方法我们可以看出,对应的增删改语句是通过Executor接口的update方法进行的,查询是通过query方法进行的。 Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) //ParameterHandler用来设置参数规则,当StatementHandler使用prepare()方法后,接下来就是使用它来设置参数。所以如果有对参数做自定义逻辑处理的时候,可以通过拦截ParameterHandler来实现 ParameterHandler (getParameterObject, setParameters) // ResultSetHandler用于对查询到的结果做处理。所以如果你有需求需要对返回结果做特殊处理的情况下可以去拦截ResultSetHandler的处理。 ResultSetHandler (handleResultSets,handleOutputParameters) //封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合 StatementHandler (prepare, parameterize,batch, update, query)
-
method 指定拦截的方法名
-
args 指定方法的参数类型,去对应的方法里看有哪些参数类型就可以了。如果填错会报错。
2.2 重写intercept(Invocation var1) 方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];//传递的参数
TableNameChange annotation = getTableNameChange(ms);//获取注解方法
if (ObjectUtil.isNotEmpty(annotation)) {
String[] originalTableNames = annotation.originalTableNames();
String[] modifiedTableNames = annotation.modifiedTableNames();
if (ObjectUtil.notEqual(originalTableNames.length, modifiedTableNames.length)) {
log.error("The property of TableNameChange is incorrectly set ");
return invocation.proceed();
}
//通过传递的changeType判断是否需要替换表名
String changeType = getParamByName(parameter, "changeType");
if (StrUtil.isNotEmpty(changeType) && StrUtil.equals(changeType, TableNameChangeType.CHANGE.name())) {
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
boundSql = (BoundSql) args[5];
}
// 获取执行的SQL
String sql = boundSql.getSql();
// 获取执行的参数
// 判断是否存在需要替换的表,不存在放行
String s = sql;
for (int i = 0; i < originalTableNames.length; i++) {
if (sql.contains(originalTableNames[i])) {
s = sql.replaceAll(originalTableNames[i], modifiedTableNames[i]);
}
// 替换执行的的SQL
}
ReflectUtil.setFieldValue(boundSql, "sql", s);
}
}
} catch (Exception ex) {
log.error(ex.getMessage());
}
return invocation.proceed();
}
2.2.1获取注解方法
private TableNameChange getTableNameChange(MappedStatement mappedStatement) {
TableNameChange annotation = null;
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
if (methodName.contains("WithChangeTableName")) {//限制方法名称
final Method[] method = Class.forName(className).getMethods();
for (Method me : method) {
if (me.getName().equals(methodName) && me.isAnnotationPresent(TableNameChange.class)) {
annotation = me.getAnnotation(TableNameChange.class);
}
}
}
} catch (Exception ex) {
log.error(ex.getMessage());
}
return annotation;
}
2.2.2 根据属性名获取属性
private String getParamByName(Object parameter, String paramName) {
String value = null;
if (parameter instanceof Map) {
Map param = (Map) parameter;
if (!param.containsKey(paramName)) {
int size = param.keySet().size() / 2;
for (int i = 1; i <= size; i++) {
String key = "param" + i;
Object o = param.get(key);
if (o instanceof QueryWrapper) {
QueryWrapper q = (QueryWrapper) o;
String sqlSegment = q.getExpression().getSqlSegment();
String s = paramName + " =";
if (sqlSegment.contains(s)) {
String pKey = sqlSegment.split(s)[1].split("paramNameValuePairs.")[1].split("}")[0];
value = q.getParamNameValuePairs().get(pKey).toString();
break;
}
}
}
} else {
value = param.get(paramName).toString();
}
} else if (parameter instanceof String) {
value = parameter.toString();
} else {
JSONObject json = (JSONObject) JSON.toJSON(parameter);
value = json.get(paramName).toString();
}
return value;
}
三、小结
如何实现在一个表中查询结果为null,自动更换表名去另外一个表中查询
//先执行一下invocation.proceed(),判断为空,再修改表名执行一次
Object proceed = invocation.proceed();
if (ObjectUtil.isNotEmpty(proceed)) {
return proceed;
}
// 判断是否存在需要替换的表,不存在放行
if (sql.contains(annotation.originalTableName())) {
String s = sql.replaceAll(annotation.originalTableName(), annotation.modifiedTableName());
// 替换执行的的SQL
ReflectUtil.setFieldValue(boundSql, "sql", s);
} else {
return proceed;
}
}
}
return invocation.proceed();
此时需要执行两次invocation.proceed();
最终反射调用的native本地方法传参如下图,虽然参数变了,但是并没有打印第二个sql语句,所以还是没有结果,我想可能是因为默认开启了mybatis的一级缓存,在一个会话中有效,导致第二个sql没有执行。而事实确实如此!!!
关闭一级缓存
mybatis:
configuration:
cache-enabled: false #禁用二级缓存
local-cache-scope: statement #禁用了一级缓存