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




























