Mybatis 的 Interceptor(攔截器) 與 JSqlparser 結(jié)合解析SQL 使SpringBoot項(xiàng)目多數(shù)據(jù)庫兼容的嘗試(推薦)
Mybaits插件簡單描述
插件簡介和具體作用
Mybatis官方提供了插件機(jī)制為用戶開辟了一道可以自定義的攔截?cái)U(kuò)展功能,在 系統(tǒng)最終執(zhí)行SQL 之前,分別有四個(gè)部位可以做擴(kuò)展,允許用戶在不修改Mybatis核心代碼的情況下,添加自己的邏輯處理,去完成各種各樣的業(yè)務(wù)場景
典型案例:
2.分頁插件 | MyBatis-Plus (baomidou.com)
業(yè)務(wù)場景示例
- 性能監(jiān)控:記錄SQL執(zhí)行時(shí)間,統(tǒng)計(jì)慢查詢,幫助開發(fā)者發(fā)現(xiàn)并優(yōu)化性能瓶頸
- 訪問控制:根據(jù)用戶的權(quán)限,動(dòng)態(tài)修改SQL語句,實(shí)現(xiàn)數(shù)據(jù)行級或列級的權(quán)限控制。
- 動(dòng)態(tài)SQL注入:根據(jù)不同的業(yè)務(wù)需求,動(dòng)態(tài)拼接或修改SQL語句。
- 數(shù)據(jù)脫敏:在查詢結(jié)果返回給客戶端之前,對敏感信息進(jìn)行脫敏處理。
- 緩存控制:對查詢結(jié)果進(jìn)行緩存,減少數(shù)據(jù)庫訪問次數(shù),提高系統(tǒng)性能。
- 自定義分頁:用戶也可以像pageHelper、mybatis-plus的分頁那樣自定義自己的分頁
- 加密解密:利用這個(gè)機(jī)制,對數(shù)據(jù)庫中敏感字段的對稱加密,諸如密碼、卡號(hào)、身份信息之類的
- 自定義日志輸出:攔截SQL執(zhí)行,實(shí)現(xiàn)自定義的日志輸出格式,便于問題追蹤和調(diào)試。
- 動(dòng)態(tài)數(shù)據(jù)源切換:在攔截器中根據(jù)業(yè)務(wù)規(guī)則,動(dòng)態(tài)選擇不同的數(shù)據(jù)源。
- 參數(shù)校驗(yàn):在執(zhí)行SQL語句前,對參數(shù)進(jìn)行校驗(yàn),確保數(shù)據(jù)的完整性和一致性。
四個(gè)核心組件簡介
這四個(gè)部位其實(shí)就是Mybatis的四大核心組件:
- Executor (執(zhí)行器):負(fù)責(zé)增刪改查和事務(wù),它調(diào)度(另外三個(gè))
StatementHandler、ParameterHandler、ResultSetHandler等來執(zhí)行對應(yīng)的SQL - StatementHandler(語句預(yù)處理) : 封裝
JDBC,構(gòu)建SQL語法,負(fù)責(zé)和數(shù)據(jù)庫進(jìn)行交互執(zhí)行sql語句,(后期下手操作和修改SQL也主要是以它為主) - ParameterHandler (參數(shù)處理): 負(fù)責(zé)將參數(shù)真正加入到SQL語句中的部分
- ResultSetHandler (結(jié)果處理) :負(fù)責(zé)將JDBC查詢結(jié)果映射到j(luò)ava對象
以下是流程圖:

攔截方法劃分
精細(xì)劃分的目的是為了允許在數(shù)據(jù)庫操作的不同階段進(jìn)行精確的干預(yù)和攔截
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
@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})})
@Component
public class MyApplicationInterceptor implements Interceptor {
// 具體實(shí)現(xiàn)內(nèi)容...
}可攔截的方法:
update:負(fù)責(zé)執(zhí)行 insert、update、delete 三種類型的 SQL 語句。query:負(fù)責(zé)執(zhí)行 select 類型的 SQL 語句。queryCursor:負(fù)責(zé)執(zhí)行 select 類型的 SQL 語句,返回 Cursor 對象。flushStatements:提交批處理語句,返回批處理結(jié)果。commit:事務(wù)提交。rollback:事務(wù)回滾。getTransaction:獲取事務(wù)對象。close:關(guān)閉 executor,同時(shí)根據(jù)參數(shù)決定是否強(qiáng)制回滾未提交的事務(wù)。isClosed:檢查 executor 是否已經(jīng)關(guān)閉。clearLocalCache:清除本地緩存。
ParameterHandler (getParameterObject, setParameters)
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@Slf4j
public class ParameterPluginInterceptor implements Interceptor {}- 可攔截的方法:
getParameterObject:此方法用于獲取 SQL 參數(shù)對象。setParameters:此方法將 SQL 命令中的參數(shù)與實(shí)際的參數(shù)對象相匹配。它負(fù)責(zé)將傳入的參數(shù)設(shè)置到 PreparedStatement 中。
StatementHandler (prepare, parameterize, batch, update, query)
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Slf4j
public class StatementPluginInterceptor implements Interceptor {}prepare:準(zhǔn)備一個(gè)數(shù)據(jù)庫 Statement 對象以待執(zhí)行。這個(gè)方法根據(jù)配置和上下文信息來創(chuàng)建一個(gè) PreparedStatement 或 CallableStatement 對象。parameterize:在 SQL 語句被執(zhí)行之前,該方法負(fù)責(zé)將 SQL 參數(shù)設(shè)置到 PreparedStatement 對象中。batch:負(fù)責(zé)處理批量執(zhí)行的邏輯,將多個(gè)更新語句作為一個(gè)批處理提交。update:執(zhí)行寫操作(insert、update、delete)的 SQL 語句。query:執(zhí)行查詢操作(select)的 SQL 語句,并返回結(jié)果。queryCursor:負(fù)責(zé)執(zhí)行查詢操作(select)SQL 語句,返回 Cursor 對象。getBoundSql:返回 BoundSql 對象,這個(gè)對象包含了要執(zhí)行的 SQL 語句以及該語句中所需的參數(shù)信息。
ResultSetHandler (handleResultSets, handleOutputParameters)
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
/*@Component*/
@Slf4j
public class ResultInterceptor implements Interceptor {handleResultSets:這是主要的方法之一,它接受一個(gè) Statement 對象作為參數(shù),并將 SQL執(zhí)行的結(jié)果 ResultSet 映射到結(jié)果對象。handleOutputParameters:當(dāng)存儲(chǔ)過程調(diào)用完成之后,這個(gè)方法會(huì)處理其輸出參數(shù)。它同樣接受一個(gè) Statement 對象作為參數(shù)。
攔截器實(shí)現(xiàn):
如何實(shí)現(xiàn)一個(gè)自己的攔截器
implements Interceptor 以實(shí)現(xiàn)一個(gè)Mybatis的攔截器,之后必須實(shí)現(xiàn)以上三個(gè)方法
- intercept 攔截 : 主要實(shí)現(xiàn)攔截邏輯的地方,也就是用戶拿來自定義業(yè)務(wù)的地方
- plugin:包裝方法,用來創(chuàng)建代理對象
- setProperties:配置方法,用來設(shè)置攔截器的屬性

以下是一個(gè)空白的Executor攔截:集成Interceptor之后,再加入注解內(nèi)容選擇要攔截的部分
@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})})
@Component
@Slf4j
public class ExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getTarget();
log.info("==> ExecutorInterceptor: {}", mappedStatement.getId());
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}Invocation中有什么
以下是一個(gè)使用mybaits interceptor攔截器,攔截StatementHandler

可以看到 invocation 中已經(jīng)夾了很多做攔截器需要的內(nèi)容了
最重要的東西就是
- BoundSql(解析后的sql對象) 首先它能拿到這次執(zhí)行中的SQL,其次就是參數(shù),等同于拿到一個(gè)完全可以執(zhí)行的SQL語句。很多IDEA里Mybaits log這樣的插件就是根據(jù)這個(gè)東西這么來的。
- BoundSql—ParameterObject對象 顧名思義,就是SQL的參數(shù)對象,它直接接受到我們從外界傳到內(nèi)部的參數(shù)對象,它是一個(gè)Object,根據(jù)傳遞的類型而改變
BoundSql—ParameterMapping對象 再mybatis的 mapper xml中寫的每一個(gè)#{var},最后都會(huì)形成一個(gè) ?,這和我們直接使用jdbc是一個(gè)道理,最終都是挨個(gè)往 ?(問號(hào)) 里面塞值,只不過ParameterMapping記錄了這些變量,它會(huì)在SQL最終要執(zhí)行的時(shí)候去設(shè)置參數(shù)
實(shí)現(xiàn)一個(gè)簡單的SQL語句攔截
以下實(shí)現(xiàn)了一個(gè)簡單的攔截器,攔截MappedStatement
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.util.Properties;
@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})})
@Component
@Slf4j
public class ExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf('.'));
String methodName = id.substring(id.lastIndexOf('.') + 1);
BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
Object parameterObject = boundSql.getParameterObject();
log.info("==> id: {}",id);
log.info("==> ClassName: {}", className);
log.info("==> MethodName: {}", methodName);
log.info("==> SQL語句: {}", boundSql.getSql());
log.info("==> 參數(shù): {}", parameterObject);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}最終效果:(可以看到,打印了SQL的來源,包、方法、SQL語句、參數(shù)都拿到了)

攔截SQL執(zhí)行的最終結(jié)果
ResultSetHandler是對SQL最終操作的結(jié)果映射到j(luò)ava中的步驟,以下代碼的操作,最終將攔截這個(gè)步驟,并對MAP相關(guān)結(jié)果做出操作,將MAP中的Key都轉(zhuǎn)成大寫
@Slf4j
@Component
@Intercepts(
{@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)})
public class ResultMapCaseInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();
MetaObject metaResultSetHandler = MetaObject.forObject(resultSetHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement = (MappedStatement) metaResultSetHandler.getValue("mappedStatement");
Object returnValue = invocation.proceed();
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
String resultMapTypeName = resultMaps.get(0).getType().getName();
String MAP = "java.util.Map";
String HASH_MAP = "java.util.HashMap";
String LINKED_HASH_MAP = "java.util.LinkedHashMap";
if(StrUtil.equalsIgnoreCase(MAP, resultMapTypeName) ||
StrUtil.equalsIgnoreCase(HASH_MAP, resultMapTypeName) ||
StrUtil.equalsIgnoreCase(LINKED_HASH_MAP, resultMapTypeName)){
if(returnValue instanceof List<?> list){
if(CollectionUtil.isNotEmpty(list)) {
list.forEach(item -> {
Map<String, Object> map = (Map<String, Object>) item;
Map<String, Object> newMap = new LinkedHashMap<>();
map.forEach((k, v) -> {
newMap.put(k.toUpperCase(), v);
});
map.clear();
map.putAll(newMap);
});
}
}
}
return returnValue;
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
}
}使用JSQLparser解析SQL語句
簡述及安裝
JSqlParser 是一個(gè) SQL 語句解析器。它轉(zhuǎn)換 Java 類的可遍歷層次結(jié)構(gòu)中的 SQL。JSqlParser不僅限于一個(gè)數(shù)據(jù)庫,而是支持Oracle,SqlServer,MySQL,PostgreSQL等等,至今它仍在更新,而Mybatisplus之中也包含了這個(gè)庫,可以直接使用,如果沒有使用mybatis plus那么需要手動(dòng)引入它
官方網(wǎng)站:JSQLParser 4.9 documentation
Github:JSQLParser/JSqlParser(github.com)
Maven:
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.9</version>
</dependency>Gradle/KT:
implementation("com.github.jsqlparser:jsqlparser:4.9")簡單的使用
以下是一個(gè)簡單的例子,為了展示Jsqlparser可以解析SQL的內(nèi)容
@Test
public void Test2() throws JSQLParserException, ParseException {
String originalSql = "SELECT " +
"t1.id, " +
"t1.name, " +
"SUM(t2.amount) AS total_amount, " +
"(SELECT COUNT(*) FROM orders o WHERE o.customer_id = t1.id) AS order_count " +
"FROM customers t1 " +
"JOIN orders t2 ON t1.id = t2.customer_id " +
"LEFT JOIN payments t3 ON t2.id = t3.order_id " +
"WHERE t1.status = 'active' " +
"AND t2.order_date BETWEEN '2023-01-01' AND '2023-12-31' " +
"GROUP BY t1.id, t1.name " +
"HAVING SUM(t2.amount) > 1000 " +
"ORDER BY total_amount DESC, t1.name ASC;";
CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
Statement statement = parser.Statement();
parser.getASTRoot().jjtAccept(sqlModifier, null);
log.info("==> JsqlParser SQL: {}", statement.toString());
Select selectStatement = (Select) statement;
PlainSelect plainSelect = selectStatement.getPlainSelect();
Table table = (Table) plainSelect.getFromItem();
System.out.println(table.toString());
//獲取表名們
Set<String> tableNames = TablesNamesFinder.findTables(originalSql);
System.out.println("表名們:"+tableNames);
// Print SELECT clause
System.out.println("SELECT clause: " + plainSelect.getSelectItems());
// Print FROM clause
System.out.println("FROM clause: " + plainSelect.getFromItem());
// Print JOIN clauses
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
System.out.println("JOIN clause: " + join);
}
}
// Print WHERE clause
System.out.println("WHERE clause: " + plainSelect.getWhere());
// Print GROUP BY clause
System.out.println("GROUP BY clause: " + plainSelect.getGroupBy());
// Print HAVING clause
System.out.println("HAVING clause: " + plainSelect.getHaving());
// Print ORDER BY clause
System.out.println("ORDER BY clause: " + plainSelect.getOrderByElements());
}直接使用
以下是一個(gè)簡單的使用,在代碼里使用Jsqlparser,最直接的方式就是直接調(diào)用CCJSqlParserUtil
String originalSql = "select * from t_user" // 需要解析的SQL語句 CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql); Statement statement = parser.Statement();
拆解SQL語句
通過 getPlainSelect() 獲得Statement中的PlainSelect,而這個(gè)PlainSelect就可以拿到很多SQL語句中的內(nèi)容

獲取各種子元素
String originalSql = "SELECT rrr.* FROM rel_role_resource rrr " +
"JOIN rel_role_user rru ON rrr.role_id = rru.role_id " +
"JOIN `user` u ON u.id = rru.user_id " +
"AND rrr.org_id = ? " +
"AND rrr.role_id = rru.role_id " +
"AND u.id = 123";
CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
Statement statement = parser.Statement();
Select select = (Select) statement;
log.info("==> Select: {}", select.toString());
PlainSelect plain = select.getPlainSelect();
plain.getSelectItems().forEach(selectItem -> {
log.info("==> 查詢的字段: {}", selectItem.toString());
});
if (plain.getFromItem() != null) {
log.info("==> 查詢的表: {}", plain.getFromItem().toString());
}
if (plain.getWhere() != null){
log.info("==> 查詢的條件: {}", plain.getWhere().toString());
}
if(CollectionUtil.isNotEmpty(plain.getJoins())){
plain.getJoins().forEach(join -> {
join.getOnExpressions().forEach(expression -> {
log.info("==> Join 條件: {}", expression.toString());
});
log.info("==> Join: {}", join.toString());
});
}
TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
Set<String> tableList = tablesNamesFinder.getTables(statement);
tableList.forEach(table -> {
log.info("==> 查詢的表: {}", table);
});
log.info("==> 查詢的表: {}", tableList);運(yùn)行后結(jié)果:

關(guān)于Jsqlparser的具體使用,可以看另外一篇博客【JSqlParser】Java使用JSqlParser解析SQL語句總結(jié)
總結(jié)
使用Springboot + mybatis + Jsqlparser,通過Mybatis的interceptor,可以獲取到整個(gè)項(xiàng)目中通過mybatis執(zhí)行的SQL語句,最終在StatementHandler 中截取SQL,對截取到的SQL進(jìn)行分析,再通過Jsqlparser的visit 對SQL 的不同位置,例如替換 引號(hào)、特殊字符、函數(shù)等,做處理,最終可以達(dá)到數(shù)據(jù)庫遷移并適配的效果
到此這篇關(guān)于Mybatis 的 Interceptor(攔截器) 與 JSqlparser 結(jié)合解析SQL 使SpringBoot項(xiàng)目多數(shù)據(jù)庫兼容的嘗試(推薦)的文章就介紹到這了,更多相關(guān)Mybatis Interceptor攔截器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- mybatis-plus的分頁攔截器(PaginationInterceptor)分頁失敗問題及解決
- MybatisPlusInterceptor實(shí)現(xiàn)sql攔截器超詳細(xì)教程
- mybatis 自定義實(shí)現(xiàn)攔截器插件Interceptor示例
- Mybatis Interceptor 攔截器的實(shí)現(xiàn)
- Mybatis Plus JSqlParser解析sql語句及JSqlParser安裝步驟
- MyBatis-Plus與PageHelper依賴的jsqlparser庫沖突
- mybatis-plus報(bào)錯(cuò)net.sf.jsqlparser.statement.select.SelectBody的問題解決
- SpringBoot基于Mybatis攔截器和JSqlParser實(shí)現(xiàn)數(shù)據(jù)隔離
相關(guān)文章
使用Java8?Stream流的skip?+?limit實(shí)現(xiàn)批處理的方法
Stream 作為 Java 8 的一大亮點(diǎn),它與 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念這篇文章主要介紹了使用Java8?Stream流的skip?+?limit實(shí)現(xiàn)批處理,需要的朋友可以參考下2022-07-07
解決Feign調(diào)用的GET參數(shù)傳遞的問題
這篇文章主要介紹了解決Feign調(diào)用的GET參數(shù)傳遞的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
RabbitMQ實(shí)現(xiàn)延遲通知的兩種方案
延遲通知是指消息在發(fā)送后不會(huì)立即被消費(fèi),而是在指定的時(shí)間延遲后才被處理的消息傳遞機(jī)制,2025-10-10
解決Nacos在執(zhí)行startup.cmd的時(shí)候出現(xiàn)閃退的問題
因?yàn)樵诠ぷ髦械捻?xiàng)目中需要使用到nacos作為注冊中心,但是在使用nacos的過程中運(yùn)行startup.cmd的時(shí)候出現(xiàn)了閃退的情況,運(yùn)行startup.cmd閃一下就沒有了,我把解決這個(gè)問題的全過程理了一下,希望能幫到您,需要的朋友可以參考下2023-12-12
Jackson中json格式的字符串與對象的互相轉(zhuǎn)換方式
這篇文章主要介紹了Jackson中json格式的字符串與對象的互相轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07

