MyBatis 的 SQL 攔截器:原理、實(shí)現(xiàn)與實(shí)踐
前言
在MyBatis框架的使用過(guò)程中,我們常常需要對(duì)SQL執(zhí)行過(guò)程進(jìn)行干預(yù) —— 比如打印執(zhí)行日志、統(tǒng)計(jì)執(zhí)行時(shí)間、動(dòng)態(tài)修改SQL語(yǔ)句,甚至實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制。而MyBatis提供的SQL攔截器(Interceptor)機(jī)制,正是實(shí)現(xiàn)這些需求的核心工具。
核心原理
MyBatis的SQL攔截器本質(zhì)上是基于JDK動(dòng)態(tài)代理實(shí)現(xiàn)的插件機(jī)制,它允許開發(fā)者在 SQL 執(zhí)行的關(guān)鍵節(jié)點(diǎn)插入自定義邏輯。要理解其原理,需先明確兩個(gè)核心概念:攔截目標(biāo)與代理機(jī)制。
核心接口
- Executor:MyBatis的核心執(zhí)行器,負(fù)責(zé)SQL的整體執(zhí)行(如select、update、commit等),是最常用的攔截目標(biāo)。
 - StatementHandler:處理SQL語(yǔ)句的準(zhǔn)備(如創(chuàng)建 Statement)、參數(shù)設(shè)置、結(jié)果集映射等,可用于修改SQL語(yǔ)句或參數(shù)。
 - ParameterHandler:處理SQL參數(shù)的設(shè)置(如為PreparedStatement設(shè)置參數(shù)),適合攔截參數(shù)并進(jìn)行加工。
 - ResultSetHandler:處理查詢結(jié)果集的映射(如將結(jié)果映射為Java對(duì)象),可用于修改返回結(jié)果。
 
代理機(jī)制
MyBatis的攔截器通過(guò)動(dòng)態(tài)代理 + 責(zé)任鏈模式工作:當(dāng)定義一個(gè)攔截器后,MyBatis會(huì)為被攔截的接口生成代理對(duì)象,將攔截邏輯嵌入代理對(duì)象中;若存在多個(gè)攔截器,則會(huì)形成代理鏈(外層代理調(diào)用內(nèi)層代理,最終調(diào)用原始對(duì)象)。 具體流程如下:
- 攔截器通過(guò)@Intercepts注解聲明攔截目標(biāo)(接口、方法、參數(shù));
 - MyBatise 啟動(dòng)時(shí)掃描攔截器,為目標(biāo)接口創(chuàng)建代理對(duì)象;
 - 當(dāng)調(diào)用目標(biāo)接口的方法時(shí),代理對(duì)象先執(zhí)行攔截器的intercept方法(自定義邏輯),再調(diào)用原始方法;
 - 若有多個(gè)攔截器,代理對(duì)象會(huì)按順序執(zhí)行所有攔截邏輯后,再執(zhí)行原始方法。
 
實(shí)現(xiàn)步驟
實(shí)現(xiàn)一個(gè)MyBatis SQL攔截器需遵循固定流程:定義攔截器類、聲明攔截目標(biāo)、實(shí)現(xiàn)攔截邏輯,最后配置生效。下面以SQL 執(zhí)行時(shí)間統(tǒng)計(jì)為例,詳解具體實(shí)現(xiàn)。
定義攔截器類:實(shí)現(xiàn) Interceptor 接口
該接口包含3個(gè)核心方法:
- intercept(Invocation invocation):核心方法,攔截邏輯的實(shí)現(xiàn)(如統(tǒng)計(jì)時(shí)間、修改參數(shù))。
 - plugin(Object target):決定是否為目標(biāo)對(duì)象生成代理(通常通過(guò)Plugin.wrap(target, this)實(shí)現(xiàn))。
 - setProperties(Properties properties):接收配置文件中傳入的參數(shù)(如攔截器開關(guān)、日志級(jí)別)。
 
// 聲明攔截目標(biāo):攔截Executor的query和update方法
@Intercepts({
    @Signature(
        type = Executor.class, // 攔截的接口
        method = "query", // 攔截的方法
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} // 方法參數(shù)(需與接口方法一致)
    ),
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    )
})
public class SqlExecuteTimeInterceptor implements Interceptor {
    // 攔截邏輯:統(tǒng)計(jì)SQL執(zhí)行時(shí)間
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 記錄開始時(shí)間
        long startTime = System.currentTimeMillis();
        try {
            // 2. 執(zhí)行原始方法(如query/update)
            return invocation.proceed();
        } finally {
            // 3. 計(jì)算執(zhí)行時(shí)間并打印
            long endTime = System.currentTimeMillis();
            long cost = endTime - startTime;
            // 獲取SQL語(yǔ)句(從MappedStatement中提?。?            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            String sqlId = mappedStatement.getId(); // Mapper接口方法全路徑
            System.out.printf("SQL執(zhí)行:%s,耗時(shí):%d ms%n", sqlId, cost);
        }
    }
    // 生成代理對(duì)象(固定寫法)
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    // 接收配置參數(shù)(如無(wú)需參數(shù)可空實(shí)現(xiàn))
    @Override
    public void setProperties(Properties properties) {
        // 例如:從配置中獲取閾值,超過(guò)閾值打印警告
        String threshold = properties.getProperty("slowSqlThreshold");
        if (threshold != null) {
            // 處理參數(shù)...
        }
    }
}聲明攔截目標(biāo):@Intercepts 與 @Signature
攔截器必須通過(guò)@Intercepts和@Signature注解明確攔截目標(biāo),否則MyBatis無(wú)法識(shí)別攔截邏輯。
- @Intercepts:包裹一個(gè)或多個(gè)@Signature,表示攔截的一組目標(biāo)。
 - @Signature:定義單個(gè)攔截目標(biāo),包含3個(gè)屬性:
 
type:被攔截的接口(如Executor、StatementHandler);
method:被攔截的方法名(如Executor的query、update);
args:被攔截方法的參數(shù)類型數(shù)組(需與接口方法參數(shù)完全一致,用于區(qū)分重載方法)。
配置攔截器:讓 MyBatis 識(shí)別攔截器
方式 1:MyBatis 原生配置(mybatis-config.xml)
<configuration>
  <plugins>
    <!-- 配置SQL執(zhí)行時(shí)間攔截器 -->
    <plugin interceptor="com.example.SqlExecuteTimeInterceptor">
      <!-- 可選:傳入?yún)?shù)(對(duì)應(yīng)setProperties方法) -->
      <property name="slowSqlThreshold" value="500"/> <!-- 慢SQL閾值:500ms -->
    </plugin>
  </plugins>
</configuration>方式 2:Spring Boot 配置(通過(guò) @Bean 注冊(cè))
@Configuration
public class MyBatisConfig {
    @Bean
    public SqlExecuteTimeInterceptor sqlExecuteTimeInterceptor() {
        SqlExecuteTimeInterceptor interceptor = new SqlExecuteTimeInterceptor();
        // 設(shè)置參數(shù)
        Properties properties = new Properties();
        properties.setProperty("slowSqlThreshold", "500");
        interceptor.setProperties(properties);
        return interceptor;
    }
}實(shí)戰(zhàn)案例
動(dòng)態(tài)修改 SQL(如數(shù)據(jù)權(quán)限控制)
對(duì)多租戶系統(tǒng),自動(dòng)在SQL中添加租戶ID條件(如where tenant_id = 123),避免手動(dòng)編寫。
@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 獲取StatementHandler及原始SQL
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
    // 獲取當(dāng)前租戶ID(從ThreadLocal或登錄上下文獲?。?    String tenantId = TenantContext.getCurrentTenantId(); // 自定義上下文類
    // 拼接租戶條件(簡(jiǎn)單示例:僅對(duì)SELECT語(yǔ)句處理)
    if (originalSql.trim().toLowerCase().startsWith("select") && tenantId != null) {
        String modifiedSql = originalSql + " and tenant_id = " + tenantId;
        // 修改SQL
        metaObject.setValue("delegate.boundSql.sql", modifiedSql);
    }
    return invocation.proceed(); // 執(zhí)行修改后的SQL
}參數(shù)加密與解密
對(duì)敏感參數(shù)(如手機(jī)號(hào)、身份證號(hào))在入庫(kù)前加密,查詢時(shí)解密。
@Override
public Object intercept(Invocation invocation) throws Throwable {
    ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(parameterHandler);
    // 獲取參數(shù)對(duì)象(如User對(duì)象)
    Object parameter = metaObject.getValue("parameterObject");
    if (parameter instanceof User) {
        User user = (User) parameter;
        // 加密手機(jī)號(hào)
        if (user.getPhone() != null) {
            user.setPhone(EncryptUtil.encrypt(user.getPhone())); // 自定義加密工具
        }
    }
    return invocation.proceed(); // 執(zhí)行參數(shù)設(shè)置
}注意事項(xiàng)
避免過(guò)度攔截,控制攔截范圍
攔截器會(huì)嵌入SQL執(zhí)行流程,過(guò)多或過(guò)頻繁的攔截會(huì)增加性能開銷(尤其是query、prepare等高頻方法)。建議:
- 僅攔截必要的接口和方法(如統(tǒng)計(jì)時(shí)間用Executor,改SQL用StatementHandler);
 - 避免在攔截邏輯中執(zhí)行耗時(shí)操作(如IO、復(fù)雜計(jì)算)。
 
處理代理對(duì)象:獲取原始對(duì)象
由于MyBatis會(huì)對(duì)目標(biāo)接口生成代理,直接調(diào)用invocation.getTarget()可能得到代理對(duì)象(而非原始對(duì)象),需通過(guò)反射或MetaObject獲取原始對(duì)象(如StatementHandler的delegate屬性)。
推薦使用MyBatis提供的SystemMetaObject工具類處理反射,避免手動(dòng)編寫反射代碼:
MetaObject metaObject = SystemMetaObject.forObject(target);
// 獲取原始StatementHandler(delegate為StatementHandler代理的原始對(duì)象)
Object originalHandler = metaObject.getValue("delegate");控制攔截器順序:@Order 或配置順序
若存在多個(gè)攔截器,執(zhí)行順序由注冊(cè)順序決定(先注冊(cè)的先執(zhí)行)。在Spring環(huán)境中,可通過(guò)@Order注解指定順序(值越小越先執(zhí)行):
@Order(1) // 第一個(gè)執(zhí)行
public class SqlLogInterceptor implements Interceptor { ... }
@Order(2) // 第二個(gè)執(zhí)行
public class SqlModifyInterceptor implements Interceptor { ... }總結(jié)
MyBatis的SQL攔截器是其插件機(jī)制的核心,通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)對(duì)SQL執(zhí)行過(guò)程的靈活干預(yù)。本文從原理(四大接口、動(dòng)態(tài)代理)、實(shí)現(xiàn)(定義攔截器、聲明目標(biāo)、配置生效)到實(shí)踐(日志統(tǒng)計(jì)、SQL修改、參數(shù)加密),全面解析了攔截器的使用。
合理使用攔截器可以簡(jiǎn)化代碼(如自動(dòng)添加租戶條件)、增強(qiáng)可觀測(cè)性(如SQL日志),但需注意性能與兼容性。















 
 
 










 
 
 
 