java-mybatis-custom interceptor interceptor-getネイティブSQL補完パブリックパラメータ
環境 pgsql + jdk1.8 + mybatis + springboot
I.はじめに
データは PGSQL に保存されます。ビジネス シナリオの拡大により、固定スキーマの使用ではビジネス ストレージ要件を満たすことができません。コード ロジックを変更して、元々コード内にハードコーディングされていたスキーマ名を変更することが急務です。動的変数形式。
最も簡単な方法は、mySchema へのすべての参照を ${schameName} に置き換えてから、schemaName を渡すようにメソッド呼び出しを変更することです。
--原查询脚本
select * from mySchema.myTable;
--替换后的形式
select * from ${mySchema}.myTable;
${mySchema} プレースホルダーが使用される理由は、連結されたテーブル名をパラメータ化してパラメーターを渡すことができないためです。
変更が必要な位置をグローバルに検索して、その数を確認します。
--查询结果为:
Find in Files 300+ matches in 30+ files
一致するファイルを分析した結果、参照場所の一部は XML 内にあり、一部は @Select、@Update などの注釈内にあることがわかりました。思い切ってあきらめてください。!!
mybatis のインターセプタを使用することを考えました。呼び出しメソッドの仮パラメータを変更する必要がなく、非侵襲的な変更を使用してパラメータ インジェクションを完了できます。まず、インターセプタの基本情報を確認します
。
2、4 台の mybatis インターセプター
Mybatis は 4 つの傍受可能なオブジェクトを提供します。
- Executor SQL の内部エグゼキュータ
- ParameterHandler はパラメータの処理をインターセプトします
- StatementHandler が SQL の構築をインターセプトする
- ResultSetHandlerインターセプト結果処理
これら 4 つの異なるタイプのインターセプタのインターセプトの順序は上から下です:
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
Executor タイプのインターセプタなど、同じタイプの 2 つのインターセプタがある場合、実行順序はインターセプタを逆の順序で SqlSessionFactory に追加します。
たとえば、Executor タイプ A インターセプタが SqlSessionFactory に追加され、Executor タイプ B インターセプタが追加された場合、B インターセプタが最初に実行され、次に A インターセプタが実行されます。
インターセプタを宣言する
- @Intercepts アノテーションと @Signature アノテーションを使用してインターセプターを宣言する
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
}
- @Signature アノテーションパラメータの説明
- type インターセプト可能な 4 つのクラスのクラス オブジェクト。
- メソッド クラスのメソッド名。上記の prepare は StatementHandler の prepare メソッドです。
- args メソッドの仮パラメータの型である量の型は、インターセプト クラスのメソッド宣言と一致します。
レジスタインターセプタ
- @Configuration を使用して注釈を構成する
@Configuration
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
/**
* mybatis 拦截器注册
*/
@PostConstruct
public void addSqlInterceptor() {
SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
3. 動的パラメータ注入スキーム:
オプション 1: 通常の SQL プレースホルダー ${...} を使用する
このアイデアは、SQL 定義で標準のプレースホルダー ${schemaName} を使用し、インターセプターの元の SQL に ${schemaName} 文字が出現するかどうかを判断し、出現する場合は schemaName パラメーターを挿入することです。
利点: 標準呼び出しと互換性があり、開発者は schemaName を渡すかどうかを選択でき、渡されない場合は自動的に挿入でき、渡されたパラメータは渡されたパラメータを表示するために使用されます。
- ParameterHandler インターセプターの使用時に発生した問題:
- ${schemaName} プレースホルダーは SQL 定義に書き込まれていますが、渡されません。これは Executor インターセプターでは異常であり、ParameterHandler インターセプターにはまったく到達できません。
- 次に、Executor インターセプターを使用してみます。
- Executor インターセプターの使用時に発生する問題:
- 受信パラメータの型が固定されておらず (マップ、ポジョ、基本型)、判断して挿入するのが不便であるため、テストは失敗しました。
@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<Object> param = new MapperMethod.ParamMap<>();
paramObj = param;
}
//执行脚本
processParam(paramObj);
RowBounds rowBounds = (RowBounds)invocation.getArgs()[2];
ResultHandler resultHandler = (ResultHandler)invocation.getArgs()[3];
return executor.query(ms, paramObj,rowBounds,resultHandler);
}
/** 处理参数对象 */
private void processParam(Object parameterObject) throws IllegalAccessException, InvocationTargetException {
//如果是map且map的key中没有需要的参数,则添加到参数map中
if (parameterObject instanceof Map) {
String schemaName = "mySchema";
((Map) parameterObject).putIfAbsent("schemaName", schemaName);
return;
}
}
}
オプション 2: 非標準のプレースホルダーを使用する
アイデア: カスタム プレースホルダーを使用すると、パラメーター定義の検証をスキップし、SQL の前処理中にプレースホルダーを正しいスキーマ名に置き換えることができます。利点: 注釈によって定義された XML および SQL との互換性があり、select、select、sql などのすべての Type sql をサポートします
。挿入、削除、更新;
欠点: 受信パラメータの表示に互換性がないため、実行されたすべての 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();
}
}
4. 結論
2 番目のスキームを使用すると、スキーマの動的な置換の要求が完全に解決されます。
プロジェクトの [ファイルで置換] を使用して、元の固定スキーマ名を _schemaName プレースホルダーに置き換えます。
コードは完璧に機能します! !!