[MybBatis の高度な記事] MyBatis インターセプター

MyBatis は、柔軟な SQL マッピングおよび実行機能を提供する人気のある Java 永続層フレームワークです。場合によっては、条件 (作成時間、変更時間) の追加、並べ替え、ページングなど、実行時に SQL ステートメントを動的に変更する必要がある場合があります。MyBatis は、この要件を達成するための強力なメカニズム、つまり を提供します拦截器(Interceptor)

インターセプターの紹介

InterceptorはAOP(アスペクト指向プログラミング)をベースにした技術で、対象オブジェクトのメソッド実行の前後にカスタムロジックを挿入することができます。MyBatis では、次の 4 種類のインターセプターを定義しています。

  • エグゼキューター: 更新、クエリ、コミット、ロールバックなど、エグゼキューターをインターセプトする方法。キャッシュ、トランザクション、ページングなどの機能を実装するために使用できます。
  • ParameterHandler: setParameters などのパラメータ ハンドラをインターセプトするメソッド。パラメータやその他の関数の変換または暗号化に使用できます。
  • ResultSetHandler: handleResultSets、handleOutputParameters などの結果セット プロセッサをインターセプトするメソッド。結果セットやその他の関数を変換またはフィルターするために使用できます。
  • StatementHandler: 準備、パラメータ化、バッチ、更新、クエリなどのステートメント プロセッサをインターセプトするメソッド。SQL ステートメントの変更、パラメータの追加、ログの記録、その他の機能に使用できます。
インターセプトされたクラス 傍受の方法
執行者 更新、クエリ、flushStatements、コミット、ロールバック、getTransaction、閉じる、isClosed
パラメータハンドラ getParameterObject、setParameters
ステートメントハンドラ 準備、パラメータ化、バッチ、更新、クエリ
結果セットハンドラ handleResultSets、handleOutputParameters

インターセプターを実装する

1.org.apache.ibatis.plugin.Interceptor インターフェイスを実装するインターセプタ クラスを定義し、interceptその中のpluginおよびメソッドを書き換えます。setProperties

public interface Interceptor {
    
    
    Object intercept(Invocation var1) throws Throwable;
    Object plugin(Object var1);
    void setProperties(Properties var1);
}
  • intercept(Invocation invocation): 上記から、インターセプターがインターセプトできるオブジェクトの 4 つのタイプがわかりました。ここで、入力パラメーターの呼び出しは、インターセプトされたオブジェクトを指します。
    例: StatementHandler#query(Statement st, ResultHandler rh)メソッドをインターセプトすると、Invocation がオブジェクトになります。
  • plugin(Object target): このメソッドの機能は、mybatis にインターセプトするかどうかを判断させ、プロキシを生成するかどうかを決定させることです。
  • setProperties(Propertiesproperties) : インターセプターにはいくつかの変数オブジェクトが必要で、このオブジェクトは構成可能性をサポートします。

2.@Interceptsアノテーションを追加し、インターセプトする必要があるオブジェクトとメソッド、およびメソッドのパラメーターを記述します。たとえば@Intercepts({@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class, Integer.class})})、SQL 実行前のインターセプト処理を意味します。

3. 設定ファイルにインターセプターを追加します。

レジスタインターセプタ

1.XML方式

<plugins>
    <plugin interceptor="xxxx.CustomInterceptor"></plugin>
</plugins>

2. Mybatis-spring-boot-start メソッド。@Component/@Beanクラスを使用してコンテナに登録するだけです

@Component
@Slf4j
@Intercepts({
    
    
        @Signature(type = StatementHandler.class, method = "prepare", args = {
    
    Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {
    
    
 ...
}

応用

メソッドに動的に切り替えられるアノテーション識別子が含まれているかどうかに応じて、SQL に含まれる情報を置き換えます。

yml

XML ファイルで置換するプレースホルダー識別子 (@dynamicSql) と置換する日付条件を指定します。

spring:
  datasource:
    #   数据源基本配置
    url: jdbc:mysql://localhost:3306/order_db_1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
    type: com.alibaba.druid.pool.DruidDataSource
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis-plus:
  configuration:
    # 驼峰转换 从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: false
    # 是否开启缓存
    cache-enable: false
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    #call-setters-on-nulls: true
    # 打印sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/*.xml


# 动态sql配置
dynamicSql:
  placeholder: "@dynamicSql"
  date: "2023-07-31"

@DynamicSql

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicSql {
    
    
}

Dao レイヤーコード

SQL プレースホルダーで置き換える必要があるメソッドに @DynamicSql アノテーションを追加します。

public interface DynamicSqlMapper  {
    
    
    @DynamicSql
    Long count();

    Long save();
}

XML

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zysheep.mapper.DynamicSqlMapper">
    <select id="count" resultType="java.lang.Long">
        select count(1) from t_order_1 where create_time > @dynamicSql
    </select>
</mapper>

スタートアップクラス

@MapperScan(basePackages = "cn.zysheep.mapper")
@SpringBootApplication
public class DmApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(DmApplication.class, args);
    }
}

インターセプタコアコード

@Component
@Slf4j
@Intercepts({
    
    
        @Signature(type = StatementHandler.class,
                method = "prepare", args = {
    
    Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {
    
    

    @Value("${dynamicSql.placeholder}")
    private String placeholder;

    @Value("${dynamicSql.date}")
    private  String dynamicDate;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    
        // 1. 获取 StatementHandler 对象也就是执行语句
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. MetaObject 是 MyBatis 提供的一个反射帮助类,可以优雅访问对象的属性,这里是对 statementHandler 对象进行反射处理,
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                new DefaultReflectorFactory());
        // 3. 通过 metaObject 反射获取 statementHandler 对象的成员变量 mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // mappedStatement 对象的 id 方法返回执行的 mapper 方法的全路径名,如cn.zysheep.mapper.DynamicSqlMapper.count
        String id = mappedStatement.getId();
        // 4. 通过 id 获取到 Dao 层类的全限定名称,然后反射获取 Class 对象
        Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
        // 5. 获取包含原始 sql 语句的 BoundSql 对象
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.info("替换前---sql:{}", sql);
        // 拦截方法
        String mSql = null;
        // 6. 遍历 Dao 层类的方法
        for (Method method : classType.getMethods()) {
    
    
            // 7. 判断方法上是否有 DynamicSql 注解,有的话,就认为需要进行 sql 替换
            if (method.isAnnotationPresent(DynamicSql.class)) {
    
    
                mSql = sql.replaceAll(placeholder, String.format("'%s'", dynamicDate));
                break;
            }
        }
        if (StringUtils.isNotBlank(mSql)) {
    
    
            log.info("替换后---mSql:{}", mSql);
            // 8. 对 BoundSql 对象通过反射修改 SQL 语句。
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, mSql);
        }
        // 9. 执行修改后的 SQL 语句。
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
    
    
        // 使用 Plugin.wrap 方法生成代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    
    
        // 获取配置文件中的属性值
    }
}

コードのテスト

@SpringBootTest(classes = DmApplication.class)
public class DynamicTest {
    
    

    @Autowired
    private DynamicSqlMapper dynamicSqlMapper;

    @Test
    public void test() {
    
    
        Long count = dynamicSqlMapper.count();
        Assert.notNull(count, "count不能为null");
    }
}

ここに画像の説明を挿入

インターセプタのアプリケーション シナリオ

1. SQL ステートメント実行監視: 実行された SQL メソッドをインターセプトし、実行された SQL ステートメント、パラメーター、その他の情報を出力し、合計実行時間を記録して、後の SQL 分析に使用できます。
2. SQL ページング クエリ: MyBatis で使用される RowBounds で使用されるメモリ ページングは​​、ページングの前に対象となるすべてのデータをクエリするため、データ量が多いとパフォーマンスが低下します。インターセプターを使用すると、クエリの前に SQL ステートメントを変更したり、必要なページング パラメーターを事前に追加したりできます。
3. パブリックフィールドの割り当て: 通常、データベースには createTime や updateTime などのパブリック フィールドが存在しますが、これらのフィールドをインターセプトすることでパラメータに一律に割り当てることができるため、set メソッドを通じて手動で値を割り当てる煩雑なプロセスが不要になります。
4. データ権限フィルタリング: 多くのシステムでは、異なるユーザーが異なるデータ アクセス権限を持っている場合があります。たとえば、マルチテナント システムでは、テナント間のデータ分離を実現するために、各テナントは自分のデータにのみアクセスできます。SQL ステートメントとインターセプタを介してパラメータを使用すると、データの自動フィルタリングを実現できます。
5. SQL ステートメントの置換: SQL 内の条件または特殊文字を論理的に置換します。(この記事の応用シナリオでもあります)

おすすめ

転載: blog.csdn.net/qq_45297578/article/details/132029683