SpringBoot集成MyBatis實(shí)現(xiàn)SQL攔截器的實(shí)戰(zhàn)指南
一、為什么需要SQL攔截器?
先看幾個(gè)真實(shí)場景:
- 慢查詢監(jiān)控:生產(chǎn)環(huán)境突然出現(xiàn)接口超時(shí),需要快速定位執(zhí)行時(shí)間過長的SQL
- 數(shù)據(jù)脫敏:用戶表查詢結(jié)果中的手機(jī)號(hào)、身份證號(hào)需要自動(dòng)替換為****
- 權(quán)限控制:多租戶系統(tǒng)中,自動(dòng)給SQL添加tenant_id = ?條件,防止數(shù)據(jù)越權(quán)訪問
- SQL審計(jì):記錄所有執(zhí)行的SQL語句、執(zhí)行人、執(zhí)行時(shí)間,滿足合規(guī)要求
如果沒有攔截器,這些需求可能需要修改每一個(gè)Mapper接口或Service方法,工作量巨大。
而MyBatis的SQL攔截器能在SQL執(zhí)行的各個(gè)階段進(jìn)行攔截處理,實(shí)現(xiàn)"無侵入式"增強(qiáng)。
二、MyBatis攔截器基礎(chǔ)
2.1 核心接口:Interceptor
MyBatis的攔截器機(jī)制基于JDK動(dòng)態(tài)代理,所有自定義攔截器都要實(shí)現(xiàn)Interceptor接口:
public interface Interceptor {
// 攔截邏輯的核心方法
Object intercept(Invocation invocation) throws Throwable;
// 生成代理對(duì)象(通常直接用Plugin.wrap())
Object plugin(Object target);
// 讀取配置參數(shù)(如從mybatis-config.xml中獲?。?
void setProperties(Properties properties);
}
2.2 攔截目標(biāo)與簽名配置
MyBatis允許攔截4個(gè)核心組件的方法,通過@Intercepts和@Signature注解指定攔截目標(biāo):

舉個(gè)栗子:攔截StatementHandler的prepare方法(SQL預(yù)編譯階段):
@Intercepts({
@Signature(
type = StatementHandler.class, // 攔截哪個(gè)接口
method = "prepare", // 攔截接口的哪個(gè)方法
args = {Connection.class, Integer.class} // 方法參數(shù)類型(用于確定重載方法)
)
})
public class MySqlInterceptor implements Interceptor {
// 實(shí)現(xiàn)接口方法...
}
注意:args參數(shù)必須嚴(yán)格匹配方法的參數(shù)類型,否則攔截不到!比如prepare方法有兩個(gè)重載,這里指定(Connection, Integer)類型的參數(shù)。
三、實(shí)戰(zhàn)一:慢查詢監(jiān)控?cái)r截器
3.1 需求說明
監(jiān)控所有SQL執(zhí)行時(shí)間,超過閾值(如500ms)則打印警告日志,包含:
- SQL執(zhí)行時(shí)間
- 完整SQL語句(帶參數(shù)占位符)
- 參數(shù)值(防止SQL注入排查)
3.2 完整實(shí)現(xiàn)代碼
(1)攔截器類
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;
@Slf4j
@Intercepts({
// 攔截查詢方法
@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class}
),
// 攔截更新方法(insert/update/delete)
@Signature(
type = StatementHandler.class,
method = "update",
args = {Statement.class}
)
})
public class SlowSqlInterceptor implements Interceptor {
// 慢查詢閾值(毫秒),可通過配置文件注入
private long slowThreshold = 500;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 記錄開始時(shí)間
long startTime = System.currentTimeMillis();
try {
// 2. 執(zhí)行原方法(繼續(xù)SQL執(zhí)行流程)
return invocation.proceed();
} finally {
// 3. 計(jì)算執(zhí)行耗時(shí)(無論成功失敗都記錄)
long costTime = System.currentTimeMillis() - startTime;
// 4. 獲取SQL語句和參數(shù)
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String sql = statementHandler.getBoundSql().getSql(); // 獲取SQL語句(帶?占位符)
Object parameterObject = statementHandler.getBoundSql().getParameterObject(); // 獲取參數(shù)
// 5. 判斷是否慢查詢
if (costTime > slowThreshold) {
log.warn("[慢查詢警告] 執(zhí)行時(shí)間: {}ms, SQL: {}, 參數(shù): {}",
costTime, sql, parameterObject);
} else {
log.info("[SQL監(jiān)控] 執(zhí)行時(shí)間: {}ms, SQL: {}", costTime, sql);
}
}
}
@Override
public Object plugin(Object target) {
// 生成代理對(duì)象(MyBatis提供的工具方法,避免自己寫代理邏輯)
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 從配置文件讀取閾值(如application.yml中配置)
String threshold = properties.getProperty("slowThreshold");
if (threshold != null) {
slowThreshold = Long.parseLong(threshold);
}
}
}
(2)SpringBoot注冊攔截器
package com.example.config;
import com.example.interceptor.SensitiveInterceptor;
import com.example.interceptor.SlowSqlInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@MapperScan("com.example.mapper") // Mapper接口所在包
public class MyBatisConfig {
// 注冊慢查詢攔截器
@Bean
public SlowSqlInterceptor slowSqlInterceptor() {
SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
// 設(shè)置屬性(也可通過application.yml配置)
Properties properties = new Properties();
properties.setProperty("slowThreshold", "500"); // 慢查詢閾值500ms
interceptor.setProperties(properties);
return interceptor;
}
@Bean
public SensitiveInterceptor sensitiveInterceptor() {
return new SensitiveInterceptor();
}
// 將攔截器添加到SqlSessionFactory
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, SlowSqlInterceptor slowSqlInterceptor) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 設(shè)置Mapper.xml路徑(如果需要)
/*sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml")
);*/
// 添加攔截器
sessionFactory.setPlugins(slowSqlInterceptor);
return sessionFactory.getObject();
}
}
(3)測試效果
寫個(gè)簡單的查詢接口:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
執(zhí)行后控制臺(tái)輸出:
[SQL監(jiān)控] 執(zhí)行時(shí)間: 30ms, SQL: SELECT id,username,phone FROM user WHERE id = ?
如果SQL執(zhí)行時(shí)間超過500ms(比如查詢大數(shù)據(jù)量表):
[慢查詢警告] 執(zhí)行時(shí)間: 1430ms, SQL: SELECT * FROM user WHERE id = ?, 參數(shù): {id=1, param1=1}
踩坑提示:如果攔截不到SQL,檢查@Signature注解的args參數(shù)是否與方法參數(shù)類型完全匹配!
四、實(shí)戰(zhàn)二:數(shù)據(jù)脫敏攔截器(敏感信息保護(hù))
4.1 需求說明
查詢用戶信息時(shí),自動(dòng)將敏感字段脫敏:
- 手機(jī)號(hào):13812345678 → 138****5678
- 身份證號(hào):110101199001011234 → ****************34
4.2 完整實(shí)現(xiàn)代碼
(1)自定義脫敏注解
import java.lang.annotation.*;
// 作用在字段上
@Target(ElementType.FIELD)
// 運(yùn)行時(shí)生效
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
// 脫敏類型(手機(jī)號(hào)、身份證號(hào)等)
SensitiveType type();
}
// 脫敏類型枚舉
public enum SensitiveType {
PHONE, // 手機(jī)號(hào)
ID_CARD // 身份證號(hào)
}
(2)實(shí)體類添加注解
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
@Sensitive(type = SensitiveType.PHONE) // 手機(jī)號(hào)脫敏
private String phone;
@Sensitive(type = SensitiveType.ID_CARD) // 身份證號(hào)脫敏
private String idCard;
}
(3)脫敏工具類
public class SensitiveUtils {
// 手機(jī)號(hào)脫敏:保留前3位和后4位
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone; // 非手機(jī)號(hào)格式不處理
}
return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");
}
// 身份證號(hào)脫敏:保留最后2位
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 18) {
return idCard; // 非身份證格式不處理
}
return idCard.replaceAll("\d{16}(\d{2})", "****************$1");
}
}
(4)結(jié)果集攔截器
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
@Slf4j
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)
})
public class SensitiveInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 執(zhí)行原方法,獲取查詢結(jié)果
Object result = invocation.proceed();
// 2. 如果結(jié)果是List,遍歷處理每個(gè)元素
if (result instanceof List<?>) {
List<?> resultList = (List<?>) result;
for (Object obj : resultList) {
// 3. 對(duì)有@Sensitive注解的字段進(jìn)行脫敏
desensitize(obj);
}
}
return result;
}
// 反射處理對(duì)象中的敏感字段
private void desensitize(Object obj) throws IllegalAccessException {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields(); // 獲取所有字段(包括私有)
for (Field field : fields) {
// 4. 檢查字段是否有@Sensitive注解
if (field.isAnnotationPresent(Sensitive.class)) {
Sensitive annotation = field.getAnnotation(Sensitive.class);
field.setAccessible(true); // 開啟私有字段訪問權(quán)限
Object value = field.get(obj); // 獲取字段值
if (value instanceof String) {
String strValue = (String) value;
// 5. 根據(jù)脫敏類型處理
switch (annotation.type()) {
case PHONE:
field.set(obj, SensitiveUtils.maskPhone(strValue));
break;
case ID_CARD:
field.set(obj, SensitiveUtils.maskIdCard(strValue));
break;
default:
break;
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可配置更多脫敏規(guī)則,此處省略
}
}
(5)注冊多個(gè)攔截器
修改MyBatisConfig,添加脫敏攔截器:
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// ... 慢查詢攔截器配置 ...
@Bean
public SensitiveInterceptor sensitiveInterceptor() {
return new SensitiveInterceptor();
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource,
SlowSqlInterceptor slowSqlInterceptor,
SensitiveInterceptor sensitiveInterceptor) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
);
// 注冊多個(gè)攔截器(注意順序!先執(zhí)行的攔截器先注冊)
sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);
return sessionFactory.getObject();
}
}
(6)測試效果
查詢用戶信息:
User user = userService.getUserById(1L); System.out.println(user); // 輸出:User(id=1, username=張三, phone=138****5678, idCard=****************34)
五、實(shí)戰(zhàn)踩坑指南
5.1 攔截器順序問題
坑:多個(gè)攔截器時(shí),注冊順序就是執(zhí)行順序。比如先注冊慢查詢攔截器,再注冊脫敏攔截器:
SQL執(zhí)行 → 慢查詢攔截器(記錄時(shí)間) → 脫敏攔截器(處理結(jié)果)
如果順序反了,脫敏攔截器會(huì)先處理結(jié)果,慢查詢攔截器記錄的SQL就看不到原始參數(shù)了。
解決:按"執(zhí)行SQL前→執(zhí)行SQL后→處理結(jié)果"的順序注冊。
5.2 攔截器簽名配置錯(cuò)誤
坑:@Signature的args參數(shù)類型寫錯(cuò),導(dǎo)致攔截不到方法。比如StatementHandler.prepare方法有兩個(gè)重載:
// 正確的參數(shù)類型
prepare(Connection connection, Integer transactionTimeout)
// 錯(cuò)誤示例:寫成了(int)
@Signature(args = {Connection.class, int.class}) // 出現(xiàn)下面的異常!java.lang.NoSuchMethodException: org.apache.ibatis.executor.statement.StatementHandler.prepare(java.sql.Connection,int)
解決:通過IDE查看方法參數(shù)類型,確保完全一致。
5.3 性能問題
坑:在攔截器中做復(fù)雜操作(如反射遍歷所有字段)會(huì)影響性能。
解決:
- 反射操作緩存Class信息
- 非必要不攔截(如只攔截查詢方法)
- 敏感字段脫敏可考慮在DTO層處理
到此這篇關(guān)于SpringBoot集成MyBatis實(shí)現(xiàn)SQL攔截器的實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)SpringBoot MyBatis攔截SQL內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot多數(shù)據(jù)源配置方式以及報(bào)錯(cuò)問題的解決
這篇文章主要介紹了SpringBoot多數(shù)據(jù)源配置方式以及報(bào)錯(cuò)問題的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
如何在Java中創(chuàng)建線程通信的四種方式你知道嗎
開發(fā)中不免會(huì)遇到需要所有子線程執(zhí)行完畢通知主線程處理某些邏輯的場景?;蛘呤蔷€程 A 在執(zhí)行到某個(gè)條件通知線程 B 執(zhí)行某個(gè)操作。下面我們來一起學(xué)習(xí)如何解決吧2021-09-09
IDEA中沒有Mapper.xml模板選項(xiàng)的處理方法
這篇文章主要介紹了IDEA中沒有Mapper.xml模板選項(xiàng)的處理方法,需其實(shí)解決方法很簡單,只需要在idea中導(dǎo)入模板即可,本文圖文的形式給大家分享解決方法,需要的朋友可以參考下2021-04-04
MyBatis-Plus輸出完整SQL(帶參數(shù))的三種方案
當(dāng)我們使用 mybatis-plus 時(shí),可能會(huì)遇到SQL 不能直接執(zhí)行,調(diào)試也不方便的情況,那么,如何打印完整 SQL(帶參數(shù))呢?本篇文章將介紹 3 種實(shí)現(xiàn)方式,并對(duì)比它們的優(yōu)缺點(diǎn),需要的朋友可以參考下2025-02-02
jvm中指定時(shí)區(qū)信息user.timezone問題及解決方式
同一份程序使用時(shí)間LocalDateTime類型,在國內(nèi)和國外部署后,返回的時(shí)間信息前端使用出問題,這篇文章主要介紹了jvm中指定時(shí)區(qū)信息user.timezone問題及解決方法,需要的朋友可以參考下2023-02-02

