SpringBoot集成MyBatis中SQL攔截器的實(shí)戰(zhàn)指南
一、為什么需要SQL攔截器?
先看幾個(gè)真實(shí)場(chǎng)景:
慢查詢監(jiān)控:生產(chǎn)環(huán)境突然出現(xiàn)接口超時(shí),需要快速定位執(zhí)行時(shí)間過(guò)長(zhǎng)的SQL
數(shù)據(jù)脫敏:用戶表查詢結(jié)果中的手機(jī)號(hào)、身份證號(hào)需要自動(dòng)替換為****
權(quán)限控制:多租戶系統(tǒng)中,自動(dòng)給SQL添加tenant_id = ?條件,防止數(shù)據(jù)越權(quán)訪問(wèn)
SQL審計(jì):記錄所有執(zhí)行的SQL語(yǔ)句、執(zhí)行人、執(zhí)行時(shí)間,滿足合規(guī)要求
如果沒(méi)有攔截器,這些需求可能需要修改每一個(gè)Mapper接口或Service方法,工作量巨大。
而MyBatis的SQL攔截器能在SQL執(zhí)行的各個(gè)階段進(jìn)行攔截處理,實(shí)現(xiàn)"無(wú)侵入式"增強(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è)核心組件的方法,通過(guò)@Intercepts和@Signature注解指定攔截目標(biāo):
| 攔截類型 | 作用 | 常用攔截方法 |
|---|---|---|
| Executor | SQL執(zhí)行器(最常用) | update、query、commit、rollback |
| StatementHandler | SQL語(yǔ)句處理器(控制SQL生成) | prepare、parameterize |
| ParameterHandler | 參數(shù)處理器(處理SQL參數(shù)) | setParameters |
| ResultSetHandler | 結(jié)果集處理器(處理查詢結(jié)果) | handleResultSets |
舉個(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 需求說(shuō)明
監(jiān)控所有SQL執(zhí)行時(shí)間,超過(guò)閾值(如500ms)則打印警告日志,包含:
- SQL執(zhí)行時(shí)間
- 完整SQL語(yǔ)句(帶參數(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 {
// 慢查詢閾值(毫秒),可通過(guò)配置文件注入
private long slowThreshold = 500;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 記錄開(kāi)始時(shí)間
long startTime = System.currentTimeMillis();
try {
// 2. 執(zhí)行原方法(繼續(xù)SQL執(zhí)行流程)
return invocation.proceed();
} finally {
// 3. 計(jì)算執(zhí)行耗時(shí)(無(wú)論成功失敗都記錄)
long costTime = System.currentTimeMillis() - startTime;
// 4. 獲取SQL語(yǔ)句和參數(shù)
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String sql = statementHandler.getBoundSql().getSql(); // 獲取SQL語(yǔ)句(帶?占位符)
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提供的工具方法,避免自己寫(xiě)代理邏輯)
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注冊(cè)攔截器
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 {
// 注冊(cè)慢查詢攔截器
@Bean
public SlowSqlInterceptor slowSqlInterceptor() {
SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
// 設(shè)置屬性(也可通過(guò)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)測(cè)試效果
寫(xiě)個(gè)簡(jiǎn)單的查詢接口:
@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í)間超過(guò)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 需求說(shuō)明
查詢用戶信息時(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); // 開(kāi)啟私有字段訪問(wèn)權(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)注冊(cè)多個(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")
);
// 注冊(cè)多個(gè)攔截器(注意順序!先執(zhí)行的攔截器先注冊(cè))
sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);
return sessionFactory.getObject();
}
}
(6)測(cè)試效果
查詢用戶信息:
User user = userService.getUserById(1L); System.out.println(user); // 輸出:User(id=1, username=張三, phone=138****5678, idCard=****************34)
五、實(shí)戰(zhàn)踩坑指南
5.1 攔截器順序問(wèn)題
坑:多個(gè)攔截器時(shí),注冊(cè)順序就是執(zhí)行順序。比如先注冊(cè)慢查詢攔截器,再注冊(cè)脫敏攔截器:
SQL執(zhí)行 → 慢查詢攔截器(記錄時(shí)間) → 脫敏攔截器(處理結(jié)果)
如果順序反了,脫敏攔截器會(huì)先處理結(jié)果,慢查詢攔截器記錄的SQL就看不到原始參數(shù)了。
解決:按"執(zhí)行SQL前→執(zhí)行SQL后→處理結(jié)果"的順序注冊(cè)。
5.2 攔截器簽名配置錯(cuò)誤
坑:@Signature的args參數(shù)類型寫(xiě)錯(cuò),導(dǎo)致攔截不到方法。比如StatementHandler.prepare方法有兩個(gè)重載:
// 正確的參數(shù)類型
prepare(Connection connection, Integer transactionTimeout)
// 錯(cuò)誤示例:寫(xiě)成了(int)
@Signature(args = {Connection.class, int.class}) // 出現(xiàn)下面的異常!
java.lang.NoSuchMethodException: org.apache.ibatis.executor.statement.StatementHandler.prepare(java.sql.Connection,int)
解決:通過(guò)IDE查看方法參數(shù)類型,確保完全一致。
5.3 性能問(wèn)題
坑:在攔截器中做復(fù)雜操作(如反射遍歷所有字段)會(huì)影響性能。
解決:
- 反射操作緩存Class信息
- 非必要不攔截(如只攔截查詢方法)
- 敏感字段脫敏可考慮在DTO層處理
六、總結(jié)與擴(kuò)展
通過(guò)SQL攔截器,我們用極少的代碼實(shí)現(xiàn)了SQL監(jiān)控和數(shù)據(jù)脫敏,避免了修改大量業(yè)務(wù)代碼。
到此這篇關(guān)于SpringBoot集成MyBatis中SQL攔截器的實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)SpringBoot集成MyBatis SQL攔截器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring IOC創(chuàng)建對(duì)象的兩種方式
這篇文章主要給大家介紹了關(guān)于Spring IOC創(chuàng)建對(duì)象的兩種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
java實(shí)現(xiàn)圖書(shū)館管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)圖書(shū)館管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
淺談java中String StringBuffer StringBuilder的區(qū)別
下面小編就為大家?guī)?lái)一篇淺談java中String StringBuffer StringBuilder的區(qū)別。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06
java自定義動(dòng)態(tài)鏈接數(shù)據(jù)庫(kù)示例
這篇文章主要介紹了java自定義動(dòng)態(tài)鏈接數(shù)據(jù)庫(kù)示例,需要的朋友可以參考下2014-02-02
java.lang.OutOfMemoryError: Java heap space錯(cuò)誤
本文主要介紹了java.lang.OutOfMemoryError: Java heap space錯(cuò)誤的問(wèn)題解決,包括內(nèi)存泄漏、數(shù)據(jù)過(guò)大和JVM堆大小配置不足,提供了解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
MyBatis中有關(guān)int和Integer的使用方式
這篇文章主要介紹了MyBatis中有關(guān)int和Integer的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼
這篇文章主要介紹了JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼,需要的朋友可以參考下2017-04-04
將本地服務(wù)注冊(cè)到nacos上的實(shí)現(xiàn)過(guò)程
文章介紹了如何在本地安裝和啟動(dòng)Nacos服務(wù),并配置本地應(yīng)用程序使用Nacos進(jìn)行服務(wù)注冊(cè),文章強(qiáng)調(diào)了啟動(dòng)日志窗口的重要性,并提供了一個(gè)簡(jiǎn)短的總結(jié),希望對(duì)讀者有所幫助2026-01-01
SpringBoot如何動(dòng)態(tài)修改Scheduled(系統(tǒng)啟動(dòng)默認(rèn)執(zhí)行,動(dòng)態(tài)修改)
這篇文章主要介紹了SpringBoot如何動(dòng)態(tài)修改Scheduled(系統(tǒng)啟動(dòng)默認(rèn)執(zhí)行,動(dòng)態(tài)修改)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07

