环境 pgsql + jdk1.8 + mybatis + springboot
数据使用PGSQL存储,由于业务场景扩充,使用固定的Schema不能满足业务存储需求,亟需修改代码逻辑将原来在代码中写死的Schema名称改为动态可变形式。
最简单的方法是将所有引用 mySchema 的地方使用 ${schameName} 替换,再修改调用方法调用处,将schemaName传入。
--原查询脚本
select * from mySchema.myTable;
--替换后的形式
select * from ${mySchema}.myTable;
之所以用${mySchema}占位符,是因为拼接表名不能使用参数化方式传参;
好吧,全局搜下需要修改的位置,看看有多少:
--查询结果为:
Find in Files 300+ matches in 30+ files
分析了匹配的文件,发现引用位置有的在 xml 中,有的在 @Select、@Update等注解中,果断放弃!!!
想到了用mybatis的拦截器,不需要修改调用方法的形参,使用非侵入性的修改就可以完成参数注入;
先看下拦截器相关基础资料。
这4中不同类型的拦截器的拦截顺序为从上到下的顺序为:
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
如果相同类型的拦截器,比如Executor类型的拦截器有两个,则执行顺序为将拦截器添加到SqlSessionFactory的逆向顺序执行;
比如SqlSessionFactory中先添加了Executor类型的A拦截器,在添加了Executor类型的B拦截器,则会先执行B拦截器,再执行A拦截器;
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
}
@Configuration
public class MybatisConfig {
@Autowired
private List sqlSessionFactoryList;
/**
* mybatis 拦截器注册
*/
@PostConstruct
public void addSqlInterceptor() {
SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
思路是在sql定义中使用标准占位符 ${schemaName},在拦截器中判断原始SQL中是否出现${schemaName}字符,出现了就注入schemaName参数。
优点:可以兼容标准调用,开发者对于schameName可传可不传,不传就可以自动注入,显示传入则使用传入的参数;
@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 拦截 Executor 的 query 方法 生成sql前将 任意参数 设置到实体中
if (invocation.getTarget() instanceof Executor ) {
//&& "query".equals(invocation.getMethod().getName())
return invokeQuery(invocation);
}
return null;
}
/** 获取原始sql */
private String getRawSQL(Invocation invocation) throws NoSuchFieldException, IllegalAccessException {
//反射获取 SqlSource 对象,通过此对象获取原始SQL
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
/** SqlSource{@link org.apache.ibatis.mapping.SqlSource}的实现类比较多,不方便在所有实现类中解析原始SQL*/
SqlSource sqlSource = ms.getSqlSource();
//通过MappedStatement.SqlSource对象获取原生sql不太靠谱!!!
if(sqlSource instanceof DynamicSqlSource) {
DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) ms.getSqlSource();
if (dynamicSqlSource == null)
return null;
//反射获取 TextSqlNode 对象
Field sqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
sqlNodeField.setAccessible(true);
TextSqlNode rootSqlNode = (TextSqlNode) sqlNodeField.get(dynamicSqlSource);
//反射获取原生sql
Field textField = rootSqlNode.getClass().getDeclaredField("text");
textField.setAccessible(true);
String sql = String.valueOf(textField.get(rootSqlNode));
return sql;
}
if(sqlSource instanceof RawSqlSource) {
RawSqlSource rawSqlSource = (RawSqlSource) ms.getSqlSource();
if (rawSqlSource == null)
return null;
//反射获取 TextSqlNode 对象
Field sqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
sqlSourceField.setAccessible(true);
StaticSqlSource staticSqlSource = (StaticSqlSource) sqlSourceField.get(rawSqlSource);
//反射获取原生sql
Field sqlField = staticSqlSource.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
String sql = String.valueOf(sqlField.get(staticSqlSource));
return sql;
}
return null;
}
private Object invokeQuery(Invocation invocation) throws Exception {
//todo 按需添加注入參數提高性能
// String sql = getRawSQL(invocation);
// if(StringUtils.isBlank(sql) || sql.indexOf(schemaParamsPlaceholder)==-1)
// return null;
Executor executor = (Executor) invocation.getTarget();
// 获取第一个参数
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// mybatis的参数对象
Object paramObj = invocation.getArgs()[1];
if (paramObj == null) {
MapperMethod.ParamMap
思路:使用自定的占位符,这样就可以跳过参数定义校验,在sql预处理时将占位符替换为正确的schema名;
优点:可以兼容xml和通过注解定义的sql,支持所有类型sql如:select、insert、delete、update;
缺点:不能兼容显示传入的参数,所有执行的sql都会被拦截;
/***
* schema参数动态注入
*/
@Log4j
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
/** SQL中的占位符 */
private static final String schemaPlaceholder = "_schemaName";
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
//
if(invocation.getTarget() instanceof StatementHandler){
if("prepare".equals(invocation.getMethod().getName()))
return invokeStatementHandlerPrepare(invocation);
}
return null;
}
private Object invokeStatementHandlerPrepare(Invocation invocation) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
log.debug("prepare~~~~~~~~~~~~~~~begin");
System.out.println(sql);
if(StringUtils.isNotEmpty(sql) && sql.indexOf(schemaPlaceholder)>-1){
String adminSchema = "mySchema";
sql = sql.replaceAll(schemaPlaceholder,adminSchema);
//通过反射回写
Field sqlNodeField = boundSql.getClass().getDeclaredField("sql");
sqlNodeField.setAccessible(true);
sqlNodeField.set(boundSql,sql);
log.debug("prepare~~~~~~~~~~~~~~~replace");
log.debug(sql);
}
log.debug("prepare~~~~~~~~~~~~~~~end");
return invocation.proceed();
}
}
使用方案二,完美解决了动态替换schame的需求。
项目中使用 Replace in Files 将原来的固定schame名称,替换为 _schemaName 占位符。
代码完美运行!!!