MyBatis 攔截器 (Interceptor) 機制允許在 SQL 執行前後攔截處理,常用於效能監控、分頁插件、資料權限過濾等。本文實作一個生產級的 SqlPerformanceInterceptor,自動記錄超過閾值的 SQL 執行時間、綁定參數與呼叫堆疊,助力慢查詢優化。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlPerformanceInterceptor implements Interceptor {
private static final long SLOW_THRESHOLD_MS = 100; // 慢查詢閾值
private static final Logger log = LoggerFactory.getLogger(SqlPerformanceInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object param = invocation.getArgs().length > 1 ? invocation.getArgs()[1] : null;
long start = System.currentTimeMillis();
try {
return invocation.proceed(); // 執行原 SQL
} finally {
long elapsed = System.currentTimeMillis() - start;
if (elapsed > SLOW_THRESHOLD_MS) {
String sql = buildSql(ms, param);
log.warn("[SLOW SQL] {} ms | ID: {} | SQL: {}", elapsed, ms.getId(), sql);
// 可選:記錄呼叫堆疊定位業務代碼
// log.debug("Stack: {}", Arrays.toString(Thread.currentThread().getStackTrace()));
}
}
}
private String buildSql(MappedStatement ms, Object param) {
BoundSql boundSql = ms.getBoundSql(param);
Configuration cfg = ms.getConfiguration();
String sql = boundSql.getSql().replaceAll("\\s+", " "); // 壓縮空白
// 參數替換 (簡易版,生產建議用 ParameterHandler)
List mappings = boundSql.getParameterMappings();
if (!mappings.isEmpty() && param != null) {
TypeHandlerRegistry registry = cfg.getTypeHandlerRegistry();
if (registry.hasTypeHandler(param.getClass())) {
sql = sql.replaceFirst("\\?", formatParam(param));
} else {
MetaObject meta = cfg.newMetaObject(param);
for (ParameterMapping pm : mappings) {
String prop = pm.getProperty();
Object val = meta.hasGetter(prop) ? meta.getValue(prop)
: boundSql.hasAdditionalParameter(prop) ? boundSql.getAdditionalParameter(prop) : null;
sql = sql.replaceFirst("\\?", formatParam(val));
}
}
}
return sql;
}
private String formatParam(Object obj) {
if (obj == null) return "NULL";
if (obj instanceof String) return "'" + obj + "'";
if (obj instanceof Date) return "'" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(obj) + "'";
return obj.toString();
}
@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
@Override public void setProperties(Properties props) { /* 可讀取 threshold 等配置 */ }
}
<plugins>
<plugin interceptor="com.example.interceptor.SqlPerformanceInterceptor">
<property name="slowThresholdMs" value="200"/>
</plugin>
</plugins>
@Configuration
public class MyBatisConfig {
@Bean
public SqlPerformanceInterceptor sqlPerformanceInterceptor() {
return new SqlPerformanceInterceptor();
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return cfg -> cfg.addInterceptor(sqlPerformanceInterceptor());
}
}
ParameterHandler 正確處理集合、陣列、TypeHandler 自訂類型。Logback AsyncAppender 或自建阻塞佇列 + 消費者執行緒,避免日誌 I/O 影響主流程效能。jsqlparser 美化輸出 SQL,提升可讀性。buildSql() 字串處理與日誌寫入。replaceFirst("?") 依序替換,若 SQL 含字面常數 ? 會誤替換,建議用正則 \\? 還要考慮預處理語句佔位符上下文。