你是否曾經(jīng)遇到過這樣的情況:在大促活動期間,用戶訪問量驟增,數(shù)據(jù)庫的壓力陡然加大,導(dǎo)致響應(yīng)變慢甚至服務(wù)中斷?更讓人頭疼的是,當(dāng)你試圖快速定位問題所在時,卻發(fā)現(xiàn)難以確定究竟是哪個業(yè)務(wù)邏輯中的 SQL 語句成為了性能瓶頸。面對這樣的困境,本篇文章提出了對 SQL 進(jìn)行 “染色” 的方法來幫助大家 一眼定位問題 SQL,而無需再在多處邏輯中輾轉(zhuǎn)騰挪。本文的思路主要受之前郭忠強(qiáng)老師發(fā)布的 如何一眼定位SQL的代碼來源:一款SQL染色標(biāo)記的簡易MyBatis插件 文章啟發(fā),我在這個基礎(chǔ)上對邏輯進(jìn)行了簡化,去除了一些無關(guān)的邏輯和工具類,并只對查詢 SQL 進(jìn)行染色,使這個插件“更輕”。此外,本文除了提供 Mybatis 攔截器的實現(xiàn)以外,還提供了針對 ibatis 框架實現(xiàn)攔截的方法,用于切入相對比較老的應(yīng)用,希望對大家有所啟發(fā)~
在文章開展之前,我們先來了解一下什么是 SQL 染色:染色的含義是 在 SQL 執(zhí)行前,在 SQL 上進(jìn)行注釋打標(biāo),標(biāo)記內(nèi)容為這條 SQL 對應(yīng)的是 Mapper 文件中的哪條 SQL 以及相關(guān)的方法執(zhí)行堆棧,如下為在 SGM 的 SQL 執(zhí)行監(jiān)控端能直接看到 SQL 染色信息:

這樣便能夠非常輕松地看到到底是什么邏輯執(zhí)行了哪段 SQL,并且經(jīng)過實際生產(chǎn)性能驗證,染色操作耗時在 0 ~ 1ms 左右:
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:0ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:0ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:1ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:1ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:1ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:0ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:1ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:0ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:0ms [JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時:1ms
現(xiàn)在我們已經(jīng)對 SQL 染色有了基本的了解,下面將介紹兩種實現(xiàn)染色的方式:Mybatis 攔截器實現(xiàn)和基于 AspectJ 織入實現(xiàn)。在接下來的內(nèi)容中我會展示染色實現(xiàn)的源碼信息,但是并不復(fù)雜,代碼量只有百行,所以大家可以直接將文章中的代碼邏輯復(fù)制到項目中實現(xiàn)即可。
快速接入 SQL 染色
Mybatis 框架應(yīng)用接入:跳轉(zhuǎn) “全量源碼” 小節(jié),復(fù)制攔截器源碼到應(yīng)用中,并在 Mybatis 攔截器配置中添加該攔截器便可以生效,注意修改源碼中 com.your.package 包路徑為當(dāng)前應(yīng)用的有效包路徑
非 Mybatis 框架應(yīng)用接入:參考 “基于 AspectJ 織入實現(xiàn)” 小節(jié),通過對 SQL 執(zhí)行相關(guān) Jar 包進(jìn)行攔截實現(xiàn)
Mybatis 攔截器實現(xiàn)
在展示具體實現(xiàn)前,我還是想通過給大家介紹原理的形式一步步將其實現(xiàn),這樣也能加深大家對 Mybatis 框架的理解,也歡迎大家閱讀、訂閱專欄 由 Mybatis 源碼暢談軟件設(shè)計。如果不想看實現(xiàn)原理,直接看實現(xiàn)的話請?zhí)D(zhuǎn) 全量源碼 小節(jié)。
攔截器的作用范圍
Mybatis 的攔截器不像 Spring 的 AOP 機(jī)制,它并不能在任意邏輯處進(jìn)行切入。在 Mybatis 源碼的 Configuration 類中,定義了它的攔截器的作用范圍,即創(chuàng)建“四大處理器”時調(diào)用的 pluginAll 方法:
public class Configuration {
// ...
protected final InterceptorChain interceptorChain = new InterceptorChain();
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
parameterObject, boundSql);
// 攔截器相關(guān)邏輯
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
resultHandler, boundSql, rowBounds);
// 攔截器相關(guān)邏輯
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
rowBounds, resultHandler, boundSql);
// 攔截器相關(guān)邏輯
return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
// 創(chuàng)建具體的 Executor 實現(xiàn)類
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 攔截器相關(guān)邏輯
return (Executor) interceptorChain.pluginAll(executor);
}
}
pluginAll 是讓攔截器生效的邏輯,它具體是如何做的呢:
public class InterceptorChain {
// 所有配置的攔截器
private final List interceptors = new ArrayList?>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 注意 target 引用不斷變化,會不斷引用已經(jīng)添加攔截器的對象
target = interceptor.plugin(target);
}
return target;
}
// ...
}
InterceptorChain 實現(xiàn)非常簡單,內(nèi)部定義了集合來保存所有配置的攔截器,執(zhí)行 pluginAll 方法時會遍歷該集合,逐個調(diào)用 Interceptor#plugin 方法來 “不斷地疊加攔截器”(interceptor.plugin 方法執(zhí)行時,target 引用不斷變更)。
注意這里使用到了 責(zé)任鏈模式,由 InterceptorChain 的命名中包含 Chain 也能聯(lián)想到該模式,之后我們在使用責(zé)任鏈時也可以考慮在命名中增加 Chain 以增加可讀性。InterceptorChain 將多個攔截器串聯(lián)在一起,每個攔截器負(fù)責(zé)其特定的邏輯處理,并在執(zhí)行完自己的邏輯后,調(diào)用下一個攔截器或目標(biāo)方法,這樣設(shè)計允許不同的攔截器之間的邏輯 解耦,同時提供了 可擴(kuò)展性。
由此可知,攔截器的作用范圍是 ParameterHandler, ResultSetHandler, StatementHandler 和 Executor 處理器(Handler),但是攔截它們又能實現(xiàn)什么效果呢?
要弄清楚這個問題,首先我們需要了解攔截器能夠切入的粒度。在 Mybatis 框架中,定義攔截器時需要使用 @Intercepts 和 @Signature 注解來 配置切入的方法,如下所示:
@Intercepts({
@Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
@Service
public class SQLMarkingInterceptor implements Interceptor {
// ...
}
每個攔截器切入的 粒度是方法級別的 的,比如在我們定義的這個攔截器中,切入的是 StatementHandler#prepare 方法,那么如果我們了解了四個處理器方法的作用是不是就能知道 Mybatis 攔截器所能實現(xiàn)的功能了?所以接下來我們簡單了解一下它們的各個方法的作用:
ParameterHandler: 核心方法 setParameters,它的作用主要是將 Java 對象轉(zhuǎn)換為 SQL 語句中的參數(shù),并處理參數(shù)的設(shè)置和映射,所以攔截器切入它能 對 SQL 執(zhí)行的入?yún)⑦M(jìn)行修改
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
ResultSetHandler: 負(fù)責(zé)將 SQL 查詢返回的 ResultSet 結(jié)果集轉(zhuǎn)換為 Java 對象,攔截器切入它的方法能 對結(jié)果集進(jìn)行處理
public interface ResultSetHandler {
/**
* 處理 Statement 對象并返回結(jié)果對象
*
* @param stmt SQL 語句執(zhí)行后返回的 Statement 對象
* @return 映射后的結(jié)果對象列表
*/
List handleResultSets(Statement stmt) throws SQLException;
/**
* 處理 Statement 對象并返回一個 Cursor 對象
* 它用于處理從數(shù)據(jù)庫中獲取的大量結(jié)果集,與傳統(tǒng)的 List 或 Collection 不同,Cursor 提供了一種流式處理結(jié)果集的方式,
* 這在處理大數(shù)據(jù)量時非常有用,因為它可以避免將所有數(shù)據(jù)加載到內(nèi)存中
*
* @param stmt SQL 語句執(zhí)行后返回的 Statement 對象
* @return 游標(biāo)對象,用于迭代結(jié)果集
*/
Cursor handleCursorResultSets(Statement stmt) throws SQLException;
/**
* 處理存儲過程的輸出參數(shù)
*
* @param cs 存儲過程調(diào)用的 CallableStatement 對象
*/
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
Executor: 它的方法很多,概括來說它負(fù)責(zé)數(shù)據(jù)庫操作,包括增刪改查等基本的 SQL 操作、管理緩存和事務(wù)的提交與回滾,所以攔截器切入它主要是 管理執(zhí)行過程或事務(wù)
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// 該方法用于執(zhí)行更新操作(包括插入、更新和刪除),它接受一個 `MappedStatement` 對象和更新參數(shù),并返回受影響的行數(shù)
int update(MappedStatement ms, Object parameter) throws SQLException;
// 該方法用于執(zhí)行查詢操作,接受 `MappedStatement` 對象(包含 SQL 語句的映射信息)、查詢參數(shù)、分頁信息、結(jié)果處理器等,并返回查詢結(jié)果的列表
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey cacheKey, BoundSql boundSql) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException;
Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 該方法用于刷新批處理語句并返回批處理結(jié)果
List flushStatements() throws SQLException;
// 該方法用于提交事務(wù),參數(shù) `required` 表示是否必須提交事務(wù)
void commit(boolean required) throws SQLException;
// 該方法用于回滾事務(wù)。參數(shù) `required` 表示是否必須回滾事務(wù)
void rollback(boolean required) throws SQLException;
// 該方法用于創(chuàng)建緩存鍵,緩存鍵用于標(biāo)識緩存中的唯一查詢結(jié)果
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 該方法用于檢查某個查詢結(jié)果是否已經(jīng)緩存在本地
boolean isCached(MappedStatement ms, CacheKey key);
// 該方法用于清空一級緩存
void clearLocalCache();
// 該方法用于延遲加載屬性
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class??> targetType);
// 該方法用于獲取當(dāng)前的事務(wù)對象
Transaction getTransaction();
// 該方法用于關(guān)閉執(zhí)行器。參數(shù) `forceRollback` 表示是否在關(guān)閉時強(qiáng)制回滾事務(wù)
void close(boolean forceRollback);
boolean isClosed();
// 該方法用于設(shè)置執(zhí)行器的包裝器
void setExecutorWrapper(Executor executor);
}
StatementHandler: 它的主要職責(zé)是準(zhǔn)備(prepare)、“承接”封裝 SQL 執(zhí)行參數(shù)的邏輯,執(zhí)行SQL(update/query)和“承接”處理結(jié)果集的邏輯,這里描述成“承接”的意思是這兩部分職責(zé)并不是由它處理,而是分別由 ParameterHandler 和 ResultSetHandler 完成,所以攔截器切入它主要是 在準(zhǔn)備和執(zhí)行階段對 SQL 進(jìn)行加工等
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
void parameterize(Statement statement) throws SQLException;
void batch(Statement statement) throws SQLException;
int update(Statement statement) throws SQLException;
List query(Statement statement, ResultHandler resultHandler) throws SQLException;
Cursor queryCursor(Statement statement) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
為了加深大家對這四個處理器的理解,了解它在查詢 SQL 執(zhí)行時作用的時機(jī),我們來看一下查詢 SQL 執(zhí)行時的流程圖:

每個聲明 SQL 查詢語句的 Mapper 接口都會被 MapperProxy 代理,接口中每個方法都會被定義為 MapperMethod 對象,借助 PlainMethodInvoker 執(zhí)行(動態(tài)代理模式和策略模式),MapperMethod 中組合了 SqlCommand 和 MethodSignature,SqlCommand 對象很重要,它的 SqlCommand#name 字段記錄的是 MappedStatement 對象的 ID 值(eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor),根據(jù)它來獲取唯一的 MappedStatement(每個 MappedStatement 對象對應(yīng) XML 映射文件中一個 , , , 或 標(biāo)簽定義),SqlCommand#type 字段用來標(biāo)記 SQL 的類型。當(dāng)方法被執(zhí)行時,會先調(diào)用 SqlSession 中的查詢方法 DefaultSqlSession#selectOne,接著由 執(zhí)行器 Executor 去承接,默認(rèn)類型是 CachingExecutor,注意在這里它會調(diào)用 MappedStatement#getBoundSql 方法獲取 BoundSql 對象,這個對象實際上最終都是在 StaticSqlSource#getBoundSql 方法中獲取的,也就是說 此時我們定義在 Mapper 文件中的 SQL 此時已經(jīng)被解析、處理好了(動態(tài)標(biāo)簽等內(nèi)容均已被處理),保存在了 BoundSql 對象中。此時,要執(zhí)行的 SQL 已經(jīng)準(zhǔn)備好了,它會接著調(diào)用 SQL 處理器 的 StatementHandler#prepare 方法創(chuàng)建與數(shù)據(jù)庫交互的 Statement 對象,其中記錄了要執(zhí)行的 SQL 信息 ,而封裝 SQL 的執(zhí)行參數(shù)則由 參數(shù)處理器 DefaultParameterHandler 和 TypeHandler 完成,ResultSet 結(jié)果的處理:將數(shù)據(jù)庫中數(shù)據(jù)轉(zhuǎn)換成所需要的 Java 對象由 結(jié)果處理器 DefaultResultSetHandler 完成。現(xiàn)在我們對攔截器的原理和查詢 SQL 的執(zhí)行流程已經(jīng)有了基本的了解,回過頭來再想一下我們的需求:“使用 Mybatis 的攔截器在 SQL 執(zhí)行前進(jìn)行打標(biāo)”,那么我們該選擇哪個方法作為切入點(diǎn)更合適呢?理論上來說在 Executor, StatementHandler 和 ParameterHandler 相關(guān)的方法中切入都可以,但實際上我們還要多考慮一步:ParameterHandler 是用來處理參數(shù)相關(guān)的,在這里切入一般我們是要對入?yún)?SQL 的入?yún)⑦M(jìn)行處理,所以不選擇這里避免為后續(xù)同學(xué)維護(hù)時增加理解成本;Executor “有時不是很合適”,它其中有兩個 query 方法,先被執(zhí)行的方法,對應(yīng)圖中 CacheExecutor 左側(cè)的直線 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler),在方法中它會去調(diào)用 MappedStatement#getBoundSql 方法獲取 BoundSql 對象 完成 SQL 的處理和解析,處理和解析后的 BoundSql 對象是我們需要進(jìn)行攔截處理的,隨后 在該方法內(nèi)部 調(diào)用另一個 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql),對應(yīng)圖中 CacheExecutor 右側(cè)的曲線 query 方法,它會將 BoundSql 作為入?yún)⑷?zhí)行查詢邏輯,結(jié)合本次需求,選擇后者切入是合適的,因為它有 BoundSql 入?yún)ⅲ瑢@個入?yún)⑦M(jìn)行打標(biāo)即可,我們來看一下 CachingExecutor 的源碼:public class CachingExecutor implements Executor { private final Executor delegate; // 先調(diào)用 @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 在方法內(nèi)部調(diào)用 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 二級緩存相關(guān)邏輯 Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key); if (list == null) { // 執(zhí)行查詢邏輯(被攔截) list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list; } } // 執(zhí)行查詢邏輯(被攔截) return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } } 它使用了靜態(tài)代理模式,其中封裝的 Executor 實現(xiàn)類型為 SimpleExecutor,在注釋中標(biāo)記了“被攔截”處的方法會讓攔截器生效。那么前文中為什么要說它“有時不是很合適”呢?我們來看一種情況,將 Mybatis 配置中的 cacheEnable 置為 false,那么在創(chuàng)建執(zhí)行器時實際類型不是 CachingExecutor 而是 SimpleExecutor,如下源碼所示:public class Configuration { public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 創(chuàng)建具體的 Executor 實現(xiàn)類 Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // false 不走這段邏輯 if (cacheEnabled) { executor = new CachingExecutor(executor); } // 攔截器相關(guān)邏輯 return (Executor) interceptorChain.pluginAll(executor); } } 當(dāng)有 SELECT 查詢語句被執(zhí)行時,它會直接調(diào)用到 BaseExecutor#query 方法中,在方法內(nèi)部調(diào)用另一個需要被攔截的 query 方法,如下所示:public abstract class BaseExecutor implements Executor { public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // cache key 緩存操作 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 需要攔截的 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } @SuppressWarnings("unchecked") @Override public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // ... } } 由于該方法是在方法內(nèi)部被調(diào)用的,所以無法使攔截器生效(動態(tài)代理),這也是說它“有時不是很合適”的原因所在。因為存在這種情況,我們現(xiàn)在也只能選擇 StatementHandler 作為切入點(diǎn)了,那么是選擇切入 StatementHandler#prepare 方法還是 StatementHandler#query 方法呢?public class SimpleExecutor extends BaseExecutor { public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 創(chuàng)建 StatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 準(zhǔn)備 Statement,其中會調(diào)用 StatementHandler#prepare 方法 stmt = prepareStatement(handler, ms.getStatementLog()); // 由 StatementHandler 執(zhí)行 query 方法 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } } 根據(jù)源碼,要被執(zhí)行打標(biāo)的 BoundSql 對象會在調(diào)用 StatementHandler#prepare 方法前會將 BoundSql 對象封裝在 StatementHandler 中,如果選擇切入 StatementHandler#prepare 方法,那么在該方法執(zhí)行前在 StatementHandler 中拿到 BoundSql 對象進(jìn)行修改便能實現(xiàn)我們的需求;如果選擇切入 StatementHandler#query 方法,同樣是需要在該方法執(zhí)行前想辦法獲取到 BoundSql 對象,但是由于此時 SQL 信息已經(jīng)被保存在了即將與數(shù)據(jù)庫交互的 Statement 對象中,它的實現(xiàn)類有很多,比如常見的 PreparedStatement,在其中獲取 SQL 字符串相對復(fù)雜,所有還是選擇 StatementHandler#prepare 方法作為切點(diǎn)相對容易。攔截器的定義和源碼解析接下來我們來對攔截器進(jìn)行實現(xiàn),首先我們先對攔截器的切入點(diǎn)進(jìn)行定義:@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class}) }) public class SQLMarkingInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // ... } } 接著來實現(xiàn)其中的邏輯:@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class}) }) public class SQLMarkingInterceptor implements Interceptor { private static final Log log = LogFactory.getLog(SQLMarkingInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { try { // 1. 找到 StatementHandler(SQL 執(zhí)行時,StatementHandler 的實際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實際類型的 StatementHandler (靜態(tài)代理模式),獲取到實際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關(guān)信息,現(xiàn)在對 SQL 信息打標(biāo)即可 marking(delegate); } } catch (Exception e) { log.error(e.getMessage(), e); } return invocation.proceed(); } } 將自定義的邏輯添加上了 try-catch,避免異常影響正常業(yè)務(wù)的執(zhí)行。在主要邏輯中,需要先在 Invocation 中找到 StatementHandler 的實際被代理的對象,它被封裝在了 RoutingStatementHandler 中,隨后在 StatementHandler 中獲取到 BoundSql 對象,對 SQL 進(jìn)行打標(biāo)即可(marking 方法)。獲取 StatementHandler攔截 StatementHandler 為什么要獲取的是 RoutingStatementHandler 類型呢?我們回到攔截器攔截 StatementHandler 生效的源碼:public class Configuration { // ... protected final InterceptorChain interceptorChain = new InterceptorChain(); public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 可以發(fā)現(xiàn)攔截器實際針對的是類型便是 RoutingStatementHandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 攔截器相關(guān)邏輯 return (StatementHandler) interceptorChain.pluginAll(statementHandler); } } 我們可以發(fā)現(xiàn)攔截器在生效時,針對的是 RoutingStatementHandler 類型,所以我們要獲取該類型,如下源碼:public class SQLMarkingInterceptor implements Interceptor { private RoutingStatementHandler getRoutingStatementHandler(Object target) throws NoSuchFieldException, IllegalAccessException { // 如果被代理,那么一直找到具體被代理的對象 while (Proxy.isProxyClass(target.getClass())) { target = Proxy.getInvocationHandler(target); } while (target instanceof Plugin) { Plugin plugin = (Plugin) target; target = getFieldValue(Plugin.class, plugin, "target", Object.class); } // 找到了 RoutingStatementHandler if (target instanceof RoutingStatementHandler) { return (RoutingStatementHandler) target; } return null; } } 源碼中前兩步為處理代理關(guān)系的邏輯,因為 RoutingStatementHandler 可能被代理,需要獲取到實際的被代理對象,找到之后返回即可。那么后續(xù)為什么還要獲取到 RoutingStatementHandler 中的被代理對象呢?我們還需要再回到 Mybatis 的源碼中:public class RoutingStatementHandler implements StatementHandler { // 代理對象 private final StatementHandler delegate; public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 在調(diào)用構(gòu)造方法時,根據(jù) statementType 字段為代理對象 delegate 賦值,那么這樣便實現(xiàn)了復(fù)雜度隱藏,只由代理對象去幫忙路由具體的實現(xiàn)即可 switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } } RoutingStatementHandler 使用了靜態(tài)代理模式,實際的類型被賦值到了 delegate 字段中,我們需要在這個對象中獲取到 BoundSql 對象,獲取 delegate 對象則通過反射來完成。染色打標(biāo) marking現(xiàn)在我們已經(jīng)獲取到了 StatementHandler delegate 對象,我們可以 SQL 進(jìn)行打標(biāo)了,但在打標(biāo)之前我們需要先思考下要打標(biāo)的內(nèi)容是什么:要清楚的知道被執(zhí)行的 SQL 是定義在 Mapper 中的哪條:聲明在 Mapper 中各個方法的唯一ID,也就是 StatementId要清楚的知道這條 SQL 被執(zhí)行時,有哪些相關(guān)方法被執(zhí)行了:方法的調(diào)用棧根據(jù)我們所需去找相關(guān)的內(nèi)容就好了,以下是源碼,需要注意的是由于所有類型的 SQL 都會執(zhí)行到 prepare 方法,但我們只對 SELECT 語句進(jìn)行打標(biāo),所以需要添加邏輯判斷:public class SQLMarkingInterceptor implements Interceptor { private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實際的 SQL String sql = boundSql.getSql().trim(); // 只對 select 打標(biāo) if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對象,獲取它的 ID 值表示它是哪條 SQL MappedStatement mappedStatement = getFieldValue( BaseStatementHandler.class, delegate, "mappedStatement", MappedStatement.class ); String mappedStatementId = mappedStatement.getId(); // 方法調(diào)用棧 String trace = trace(); // 按順序創(chuàng)建打標(biāo)的內(nèi)容 LinkedHashMap markingMap = new LinkedHashMap?>(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 打標(biāo) sql = String.format(" /* %s */ %s", marking, sql); // 反射更新 Field field = getField(BoundSql.class, "sql"); field.set(boundSql, sql); } } } 執(zhí)行打標(biāo)的邏輯是修改 BoundSql 對象,將其中的 sql 字段用打標(biāo)后的 SQL 替換掉。獲取方法調(diào)用棧的邏輯我們具體來看一下,其實并不復(fù)雜,在全量堆棧信息中將不需要關(guān)注的堆棧排除掉,需要注意將 !className.startsWith("com.your.package") 修改成有效的路徑判斷:public class SQLMarkingInterceptor implements Interceptor { private String trace() { // 全量調(diào)用棧 StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace(); if (stackTraceArray.length <= 2) { return EMPTY; } LinkedList methodInfoList = new LinkedList?>(); for (int i = stackTraceArray.length - 1 - DEFAULT_INDEX; i >= DEFAULT_INDEX; i--) { StackTraceElement stackTraceElement = stackTraceArray[i]; // 排除掉不想看到的內(nèi)容 String className = stackTraceElement.getClassName(); if (!className.startsWith("com.your.package") || className.contains("FastClassBySpringCGLIB") || className.contains("EnhancerBySpringCGLIB") || stackTraceElement.getMethodName().contains("lambda$") ) { continue; } // 過濾攔截器相關(guān) if (className.contains("Interceptor") || className.contains("Aspect")) { continue; } // 只拼接類和方法,不拼接文件名和行號 String methodInfo = String.format("%s#%s", className.substring(className.lastIndexOf('.') + 1), stackTraceElement.getMethodName() ); methodInfoList.add(methodInfo); } if (methodInfoList.isEmpty()) { return EMPTY; } // 格式化結(jié)果 StringJoiner stringJoiner = new StringJoiner(" ==> "); for (String method : methodInfoList) { stringJoiner.add(method); } return stringJoiner.toString(); } } 以上便完成了 SQL “染色” 攔截器的實現(xiàn),將其添加到 mybatis 相關(guān)的攔截器配置中就可以生效了。全量源碼import com.jd.laf.config.spring.annotation.LafValue; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.statement.BaseStatementHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.springframework.stereotype.Service; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.sql.Connection; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @Slf4j @Service @Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class}) }) public class SQLMarkingInterceptor implements Interceptor { /** * 默認(rèn)線程棧數(shù)組下標(biāo) */ private static final int DEFAULT_INDEX = 2; /** * 是否開啟SQL染色標(biāo)記 */ @LafValue("sql.marking.enable") private boolean enabled; private static final Map FIELD_CACHE = new ConcurrentHashMap?>(); @Override public Object intercept(Invocation invocation) throws Throwable { if (!enabled) { return invocation.proceed(); } try { // 1. 找到 StatementHandler(SQL 執(zhí)行時,StatementHandler 的實際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實際類型的 StatementHandler (靜態(tài)代理模式),獲取到實際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關(guān)信息,現(xiàn)在對 SQL 信息打標(biāo)即可 marking(delegate); } } catch (Exception e) { log.error(e.getMessage(), e); } return invocation.proceed(); } private RoutingStatementHandler getRoutingStatementHandler(Object target) throws NoSuchFieldException, IllegalAccessException { // 如果被代理,那么一直找到具體被代理的對象 while (Proxy.isProxyClass(target.getClass())) { target = Proxy.getInvocationHandler(target); } while (target instanceof Plugin) { Plugin plugin = (Plugin) target; target = getFieldValue(Plugin.class, plugin, "target", Object.class); } // 找到了 RoutingStatementHandler if (target instanceof RoutingStatementHandler) { return (RoutingStatementHandler) target; } return null; } /** * 打標(biāo) * 1. 要清楚的知道被執(zhí)行的 SQL 是定義在 Mapper 中的哪條 * 2. 要清楚的知道這條 SQL 被執(zhí)行時方法的調(diào)用棧 */ private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實際的 SQL String sql = boundSql.getSql().trim(); // 只對 select 打標(biāo) if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對象,獲取它的 ID 值表示它是哪條 SQL MappedStatement mappedStatement = getFieldValue( BaseStatementHandler.class, delegate, "mappedStatement", MappedStatement.class ); String mappedStatementId = mappedStatement.getId(); // 方法調(diào)用棧 String trace = trace(); // 按順序創(chuàng)建打標(biāo)的內(nèi)容 LinkedHashMap markingMap = new LinkedHashMap?>(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 打標(biāo) sql = String.format(" /* %s */ %s", marking, sql); // 反射更新 Field field = getField(BoundSql.class, "sql"); field.set(boundSql, sql); } } /** * 獲取某類型 clazz 某對象 object 下某字段 fieldName 的值 fieldClass */ private T getFieldValue(Class??> clazz, Object object, String fieldName, Class fieldClass) throws NoSuchFieldException, IllegalAccessException { // 獲取到目標(biāo)類的字段 Field field = getField(clazz, fieldName); return fieldClass.cast(field.get(object)); } private String trace() { StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace(); if (stackTraceArray.length <= DEFAULT_INDEX) { return EMPTY; } LinkedList methodInfoList = new LinkedList?>(); for (int i = stackTraceArray.length - 1 - DEFAULT_INDEX; i >= DEFAULT_INDEX; i--) { StackTraceElement stackTraceElement = stackTraceArray[i]; String className = stackTraceElement.getClassName(); if (!className.startsWith("com.your.package") || className.contains("FastClassBySpringCGLIB") || className.contains("EnhancerBySpringCGLIB") || stackTraceElement.getMethodName().contains("lambda$") ) { continue; } // 過濾攔截器相關(guān) if (className.contains("Interceptor") || className.contains("Aspect")) { continue; } // 只拼接類和方法,不拼接文件名和行號 String methodInfo = String.format("%s#%s", className.substring(className.lastIndexOf('.') + 1), stackTraceElement.getMethodName() ); methodInfoList.add(methodInfo); } if (methodInfoList.isEmpty()) { return EMPTY; } // 格式化結(jié)果 StringJoiner stringJoiner = new StringJoiner(" ==> "); for (String method : methodInfoList) { stringJoiner.add(method); } return stringJoiner.toString(); } private Field getField(Class??> clazz, String fieldName) throws NoSuchFieldException { Field field; String cacheKey = String.format("%s.%s", clazz.getName(), fieldName); if (FIELD_CACHE.containsKey(cacheKey)) { field = FIELD_CACHE.get(cacheKey); } else { field = clazz.getDeclaredField(fieldName); field.setAccessible(true); FIELD_CACHE.put(cacheKey, field); } return field; } } 基于 AspectJ 織入實現(xiàn)這種方法主要用于在未使用 Mybatis 框架的系統(tǒng)中,基于 AspectJ 實現(xiàn)對 Maven 依賴中 Jar 包類的織入,完成 SQL 染色打標(biāo)的操作。同時,這種方法并不限于此,大家可以借鑒這種方法用于其他 Jar 包的織入,而不局限于 Spring 提供的 AOP 機(jī)制,畢竟 Spring 的 AOP 只能對 Bean 進(jìn)行織入。所以在本小節(jié)中,更注重方法的介紹。添加依賴和配置插件借助 AspectJ 在 編譯期 實現(xiàn)對 Maven 依賴中 Jar 包類的織入,這與運(yùn)行時織入(如 Spring AOP 使用的代理機(jī)制)不同,編譯期織入是在生成的字節(jié)碼中直接包含切面邏輯,生成的類文件已經(jīng)包含了切面代碼。首先,需要先添加依賴: org.aspectj/groupId?> aspectjrt/artifactId?> 1.8.13/version?> /dependency?> 并且在 Maven 的 plugins 標(biāo)簽下添加 aspectj-maven-plugin 插件配置,否則無法實現(xiàn)在編譯期織入: org.codehaus.mojo/groupId?> aspectj-maven-plugin/artifactId?> 1.11/version?> !-- 解決與 Lombok 的沖突 --?> true/forceAjcCompile?> ${project.build.directory}/classes/weaveDirectory?> /weaveDirectories?> !-- JDK版本 --?> 1.8/complianceLevel?> 1.8/source?> 1.8/target?> !-- 展示織入信息 --?> true/showWeaveInfo?> UTF-8/encoding?> !-- 重點(diǎn)!配置要織入的 maven 依賴 --?> org.apache.ibatis/groupId?> ibatis-sqlmap/artifactId?> /weaveDependency?> /weaveDependencies?> /configuration?> compile/goal?> /goals?> /execution?> /executions?> /plugin?> 解決與 Lombok 的沖突配置內(nèi)容不再解釋,詳細(xì)請看 CSDN: AspectJ和lombok。重點(diǎn)需要關(guān)注的配置內(nèi)容是 weaveDependency 標(biāo)簽:配置織入依賴(詳細(xì)可參見 Maven: aspectj-maven-plugin 官方文檔),也就是說如果我們想對 SqlExecutor 進(jìn)行織入,那么需要將它對應(yīng)的 Maven 依賴添加到這個標(biāo)簽下才能生效,否則無法完成織入。完成以上內(nèi)容之后,現(xiàn)在去實現(xiàn)對應(yīng)的攔截器即可。攔截器實現(xiàn)攔截器的實現(xiàn)原理非常簡單,要織入的方法是 com.ibatis.sqlmap.engine.execution.SqlExecutor#executeQuery,這個方法的簽名如下:public void executeQuery(StatementScope statementScope, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException; 根據(jù)我們的訴求:在 SQL 執(zhí)行前對 SQL 進(jìn)行染色打標(biāo),那么可以直接在這個方法的第三個參數(shù) String sql 上打標(biāo),以下是攔截器的實現(xiàn):@Slf4j @Aspect public class SqlExecutorInterceptor { private static final int DEFAULT_INDEX = 2; @Around("execution(* com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery(..))") public Object aroundExecuteQuery(ProceedingJoinPoint joinPoint) throws Throwable { // 獲取方法參數(shù) Object[] args = joinPoint.getArgs(); String sqlTemplate = ""; Object arg2 = args[2]; if (arg2 instanceof String) { // 實際的 SQL sqlTemplate = (String) arg2; } if (StringUtils.containsIgnoreCase(sqlTemplate, "select")) { try { // SQL 聲明的 ID String mappedStatementId = ""; Object arg0 = args[0]; if (arg0 instanceof StatementScope) { StatementScope statementScope = (StatementScope) arg0; MappedStatement statement = statementScope.getStatement(); if (statement != null) { mappedStatementId = statement.getId(); } } // 方法調(diào)用棧 String trace = trace(); // 按順序創(chuàng)建打標(biāo)的內(nèi)容 LinkedHashMap markingMap = new LinkedHashMap?>(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 先打標(biāo)后SQL,避免有些平臺展示SQL時進(jìn)行尾部截斷,而看不到染色信息 String markingSql = String.format(" /* %s */ %s", marking, sqlTemplate); args[2] = markingSql; } catch (Exception e) { // 發(fā)生異常的話恢復(fù)最原始 SQL 保證執(zhí)行 args[2] = sqlTemplate; log.error(e.getMessage(), e); } } // 正常執(zhí)行邏輯 return joinPoint.proceed(args); } } 邏輯上非常簡單,獲取了 MappedStatementId 和線程的執(zhí)行堆棧以注釋的形式標(biāo)記在 SELECT 語句前,注意如果大家要 對 INSERT 語句進(jìn)行打標(biāo)時,需要將標(biāo)記打在 SQL 的最后,因為部分插件如 InsertStatementParser 會識別 INSERT,如果注釋在前,INSERT 識別會有誤報錯。驗證織入完成以上工作后,我們需要驗證攔截器是否織入成功,因為織入是在編譯期完成的,所以執(zhí)行以下 Maven 編譯命令即可:mvn clean compile 在控制臺中可以發(fā)現(xiàn)如下日志信息提示織入成功:[INFO] --- aspectj-maven-plugin:1.11:compile (default) @ --- [INFO] Showing AJC message detail for messages of types: [error, warning, fail] [INFO] Join point 'method-execution(void com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery(com.ibatis.sqlmap.engine.scope.StatementScope, java.sql.Connection, java.lang.String, java.lang.Object[], int, int, com.ibatis.sqlmap.engine.mapping.statement.RowHandlerCallback))' in Type 'com.ibatis.sqlmap.engine.execution.SqlExecutor' (SqlExecutor.java:163) advised by around advice from 'com.your.package.sqlmarking.SqlExecutorInterceptor' (SqlExecutorInterceptor.class(from SqlExecutorInterceptor.java)) 并且在相應(yīng)的 target/classes 目錄下的 SqlExecutor.class 文件中也能發(fā)現(xiàn)被織入的邏輯:public class SqlExecutor { public void executeQuery(StatementScope statementScope, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException { JoinPoint.StaticPart var10000 = ajc$tjp_0; Object[] var24 = new Object[]{statementScope, conn, sql, parameters, Conversions.intObject(skipResults), Conversions.intObject(maxResults), callback}; JoinPoint var23 = Factory.makeJP(var10000, this, this, var24); SqlExecutorInterceptor var26 = SqlExecutorInterceptor.aspectOf(); Object[] var25 = new Object[]{this, statementScope, conn, sql, parameters, Conversions.intObject(skipResults), Conversions.intObject(maxResults), callback, var23}; var26.aroundExecuteQuery((new SqlExecutor$AjcClosure1(var25)).linkClosureAndJoinPoint(69648)); } } 以上,大功告成。
審核編輯 黃宇
-
SQL
+關(guān)注
關(guān)注
1文章
789瀏覽量
46695 -
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
4019瀏覽量
68331
發(fā)布評論請先 登錄
恒訊科技解析:如何安裝MySQL并創(chuàng)建數(shù)據(jù)庫
不用編程不用聯(lián)網(wǎng),實現(xiàn)倍福(BECKHOFF)PLC對接SQL數(shù)據(jù)庫,上報和查詢數(shù)據(jù)的案例
mysql數(shù)據(jù)恢復(fù)—mysql數(shù)據(jù)庫表被truncate的數(shù)據(jù)恢復(fù)案例
數(shù)據(jù)庫慢查詢分析與SQL優(yōu)化實戰(zhàn)技巧
數(shù)據(jù)庫性能優(yōu)化指南
不用編程序無需聯(lián)外網(wǎng),將Rockwell羅克韋爾(AB)PLC的標(biāo)簽數(shù)據(jù)存入SQL數(shù)據(jù)庫
數(shù)據(jù)庫數(shù)據(jù)恢復(fù)—服務(wù)器異常斷電導(dǎo)致Oracle數(shù)據(jù)庫故障的數(shù)據(jù)恢復(fù)案例
數(shù)據(jù)庫數(shù)據(jù)恢復(fù)—MongoDB數(shù)據(jù)庫文件丟失的數(shù)據(jù)恢復(fù)案例
數(shù)據(jù)庫數(shù)據(jù)恢復(fù)—SQL Server數(shù)據(jù)庫被加密如何恢復(fù)數(shù)據(jù)?
oracle數(shù)據(jù)恢復(fù)—oracle數(shù)據(jù)庫誤執(zhí)行錯誤truncate命令如何恢復(fù)數(shù)據(jù)?
SQLSERVER數(shù)據(jù)庫是什么
MySQL數(shù)據(jù)庫是什么
不用編程不用聯(lián)網(wǎng),PLC和儀表直接對SQL接數(shù)據(jù)庫,有異常時還可先將數(shù)據(jù)緩存
如何一眼定位SQL的代碼來源:一款SQL染色標(biāo)記的簡易MyBatis插件
大促數(shù)據(jù)庫壓力激增,如何一眼定位 SQL 執(zhí)行來源?
評論