环境 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 {@Autowiredprivate List sqlSessionFactoryList;/*** mybatis 拦截器注册*/@PostConstructpublic 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 {@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {Interceptor.super.setProperties(properties);}@Overridepublic 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 对象,通过此对象获取原始SQLMappedStatement 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);//反射获取原生sqlField 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);//反射获取原生sqlField 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";@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {Interceptor.super.setProperties(properties);}@Overridepublic 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 占位符。
代码完美运行!!!