女人自慰AV免费观看内涵网,日韩国产剧情在线观看网址,神马电影网特片网,最新一级电影欧美,在线观看亚洲欧美日韩,黄色视频在线播放免费观看,ABO涨奶期羡澄,第一导航fulione,美女主播操b

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

大促數據庫壓力激增,如何一眼定位 SQL 執行來源?

京東云 ? 來源:jf_75140285 ? 作者:jf_75140285 ? 2025-06-10 11:32 ? 次閱讀

你是否曾經遇到過這樣的情況:在大促活動期間,用戶訪問量驟增,數據庫的壓力陡然加大,導致響應變慢甚至服務中斷?更讓人頭疼的是,當你試圖快速定位問題所在時,卻發現難以確定究竟是哪個業務邏輯中的 SQL 語句成為了性能瓶頸。面對這樣的困境,本篇文章提出了對 SQL 進行 “染色” 的方法來幫助大家 一眼定位問題 SQL,而無需再在多處邏輯中輾轉騰挪。本文的思路主要受之前郭忠強老師發布的 如何一眼定位SQL的代碼來源:一款SQL染色標記的簡易MyBatis插件 文章啟發,我在這個基礎上對邏輯進行了簡化,去除了一些無關的邏輯和工具類,并只對查詢 SQL 進行染色,使這個插件“更輕”。此外,本文除了提供 Mybatis 攔截器的實現以外,還提供了針對 ibatis 框架實現攔截的方法,用于切入相對比較老的應用,希望對大家有所啟發~

在文章開展之前,我們先來了解一下什么是 SQL 染色:染色的含義是 在 SQL 執行前,在 SQL 上進行注釋打標,標記內容為這條 SQL 對應的是 Mapper 文件中的哪條 SQL 以及相關的方法執行堆棧,如下為在 SGM 的 SQL 執行監控端能直接看到 SQL 染色信息:

wKgZO2hHp0CAXmO7ABQZuThdHGQ262.png

這樣便能夠非常輕松地看到到底是什么邏輯執行了哪段 SQL,并且經過實際生產性能驗證,染色操作耗時在 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

現在我們已經對 SQL 染色有了基本的了解,下面將介紹兩種實現染色的方式:Mybatis 攔截器實現和基于 AspectJ 織入實現。在接下來的內容中我會展示染色實現的源碼信息,但是并不復雜,代碼量只有百行,所以大家可以直接將文章中的代碼邏輯復制到項目中實現即可。

快速接入 SQL 染色

Mybatis 框架應用接入:跳轉 “全量源碼” 小節,復制攔截器源碼到應用中,并在 Mybatis 攔截器配置中添加該攔截器便可以生效,注意修改源碼中 com.your.package 包路徑為當前應用的有效包路徑

非 Mybatis 框架應用接入:參考 “基于 AspectJ 織入實現” 小節,通過對 SQL 執行相關 Jar 包進行攔截實現

Mybatis 攔截器實現

在展示具體實現前,我還是想通過給大家介紹原理的形式一步步將其實現,這樣也能加深大家對 Mybatis 框架的理解,也歡迎大家閱讀、訂閱專欄 由 Mybatis 源碼暢談軟件設計。如果不想看實現原理,直接看實現的話請跳轉 全量源碼 小節。

攔截器的作用范圍

Mybatis 的攔截器不像 Spring 的 AOP 機制,它并不能在任意邏輯處進行切入。在 Mybatis 源碼的 Configuration 類中,定義了它的攔截器的作用范圍,即創建“四大處理器”時調用的 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);
        // 攔截器相關邏輯
        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);
        // 攔截器相關邏輯
        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);
        // 攔截器相關邏輯
        return (StatementHandler) interceptorChain.pluginAll(statementHandler);
    }

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        // 創建具體的 Executor 實現類
        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);
        }
        // 攔截器相關邏輯
        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 引用不斷變化,會不斷引用已經添加攔截器的對象
            target = interceptor.plugin(target);
        }
        return target;
    }

    // ...
}

InterceptorChain 實現非常簡單,內部定義了集合來保存所有配置的攔截器,執行 pluginAll 方法時會遍歷該集合,逐個調用 Interceptor#plugin 方法來 “不斷地疊加攔截器”(interceptor.plugin 方法執行時,target 引用不斷變更)。

注意這里使用到了 責任鏈模式,由 InterceptorChain 的命名中包含 Chain 也能聯想到該模式,之后我們在使用責任鏈時也可以考慮在命名中增加 Chain 以增加可讀性。InterceptorChain 將多個攔截器串聯在一起,每個攔截器負責其特定的邏輯處理,并在執行完自己的邏輯后,調用下一個攔截器或目標方法,這樣設計允許不同的攔截器之間的邏輯 解耦,同時提供了 可擴展性

由此可知,攔截器的作用范圍是 ParameterHandler, ResultSetHandler, StatementHandler 和 Executor 處理器(Handler),但是攔截它們又能實現什么效果呢?

要弄清楚這個問題,首先我們需要了解攔截器能夠切入的粒度。在 Mybatis 框架中,定義攔截器時需要使用 @Intercepts 和 @Signature 注解來 配置切入的方法,如下所示:

@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
@Service
public class SQLMarkingInterceptor implements Interceptor {
    // ...
}

每個攔截器切入的 粒度是方法級別的 的,比如在我們定義的這個攔截器中,切入的是 StatementHandler#prepare 方法,那么如果我們了解了四個處理器方法的作用是不是就能知道 Mybatis 攔截器所能實現的功能了?所以接下來我們簡單了解一下它們的各個方法的作用:

ParameterHandler: 核心方法 setParameters,它的作用主要是將 Java 對象轉換為 SQL 語句中的參數,并處理參數的設置和映射,所以攔截器切入它能 對 SQL 執行的入參進行修改

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}

ResultSetHandler: 負責將 SQL 查詢返回的 ResultSet 結果集轉換為 Java 對象,攔截器切入它的方法能 對結果集進行處理

public interface ResultSetHandler {

  /**
   * 處理 Statement 對象并返回結果對象
   *
   * @param stmt SQL 語句執行后返回的 Statement 對象
   * @return 映射后的結果對象列表
   */
   List handleResultSets(Statement stmt) throws SQLException;

  /**
   * 處理 Statement 對象并返回一個 Cursor 對象
   * 它用于處理從數據庫中獲取的大量結果集,與傳統的 List 或 Collection 不同,Cursor 提供了一種流式處理結果集的方式,
   * 這在處理大數據量時非常有用,因為它可以避免將所有數據加載到內存中
   *
   * @param stmt SQL 語句執行后返回的 Statement 對象
   * @return 游標對象,用于迭代結果集
   */
   Cursor handleCursorResultSets(Statement stmt) throws SQLException;

  /**
   * 處理存儲過程的輸出參數
   *
   * @param cs 存儲過程調用的 CallableStatement 對象
   */
  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

Executor: 它的方法很多,概括來說它負責數據庫操作,包括增刪改查等基本的 SQL 操作、管理緩存和事務的提交與回滾,所以攔截器切入它主要是 管理執行過程或事務

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    // 該方法用于執行更新操作(包括插入、更新和刪除),它接受一個 `MappedStatement` 對象和更新參數,并返回受影響的行數
    int update(MappedStatement ms, Object parameter) throws SQLException;

    // 該方法用于執行查詢操作,接受 `MappedStatement` 對象(包含 SQL 語句的映射信息)、查詢參數、分頁信息、結果處理器等,并返回查詢結果的列表
     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;

    // 該方法用于刷新批處理語句并返回批處理結果
    List flushStatements() throws SQLException;

    // 該方法用于提交事務,參數 `required` 表示是否必須提交事務
    void commit(boolean required) throws SQLException;

    // 該方法用于回滾事務。參數 `required` 表示是否必須回滾事務
    void rollback(boolean required) throws SQLException;

    // 該方法用于創建緩存鍵,緩存鍵用于標識緩存中的唯一查詢結果
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

    // 該方法用于檢查某個查詢結果是否已經緩存在本地
    boolean isCached(MappedStatement ms, CacheKey key);

    // 該方法用于清空一級緩存
    void clearLocalCache();

    // 該方法用于延遲加載屬性
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class targetType);

    // 該方法用于獲取當前的事務對象
    Transaction getTransaction();

    // 該方法用于關閉執行器。參數 `forceRollback` 表示是否在關閉時強制回滾事務
    void close(boolean forceRollback);

    boolean isClosed();

    // 該方法用于設置執行器的包裝器
    void setExecutorWrapper(Executor executor);

}

StatementHandler: 它的主要職責是準備(prepare)、“承接”封裝 SQL 執行參數的邏輯,執行SQL(update/query)和“承接”處理結果集的邏輯,這里描述成“承接”的意思是這兩部分職責并不是由它處理,而是分別由 ParameterHandler 和 ResultSetHandler 完成,所以攔截器切入它主要是 在準備和執行階段對 SQL 進行加工等

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 執行時作用的時機,我們來看一下查詢 SQL 執行時的流程圖:

wKgZPGhHp0GAQtYOAAyJ2nAsrkI077.png

每個聲明 SQL 查詢語句的 Mapper 接口都會被 MapperProxy 代理,接口中每個方法都會被定義為 MapperMethod 對象,借助 PlainMethodInvoker 執行(動態代理模式和策略模式),MapperMethod 中組合了 SqlCommand 和 MethodSignature,SqlCommand 對象很重要,它的 SqlCommand#name 字段記錄的是 MappedStatement 對象的 ID 值(eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor),根據它來獲取唯一的 MappedStatement(每個 MappedStatement 對象對應 XML 映射文件中一個 , , , 或 標簽定義),SqlCommand#type 字段用來標記 SQL 的類型。當方法被執行時,會先調用 SqlSession 中的查詢方法 DefaultSqlSession#selectOne,接著由 執行器 Executor 去承接,默認類型是 CachingExecutor,注意在這里它會調用 MappedStatement#getBoundSql 方法獲取 BoundSql 對象,這個對象實際上最終都是在 StaticSqlSource#getBoundSql 方法中獲取的,也就是說 此時我們定義在 Mapper 文件中的 SQL 此時已經被解析、處理好了(動態標簽等內容均已被處理),保存在了 BoundSql 對象中。此時,要執行的 SQL 已經準備好了,它會接著調用 SQL 處理器 的 StatementHandler#prepare 方法創建與數據庫交互的 Statement 對象,其中記錄了要執行的 SQL 信息 ,而封裝 SQL 的執行參數則由 參數處理器 DefaultParameterHandler 和 TypeHandler 完成,ResultSet 結果的處理:將數據庫中數據轉換成所需要的 Java 對象由 結果處理器 DefaultResultSetHandler 完成。現在我們對攔截器的原理和查詢 SQL 的執行流程已經有了基本的了解,回過頭來再想一下我們的需求:“使用 Mybatis 的攔截器在 SQL 執行前進行打標”,那么我們該選擇哪個方法作為切入點更合適呢?理論上來說在 Executor, StatementHandler 和 ParameterHandler 相關的方法中切入都可以,但實際上我們還要多考慮一步:ParameterHandler 是用來處理參數相關的,在這里切入一般我們是要對入參 SQL 的入參進行處理,所以不選擇這里避免為后續同學維護時增加理解成本;Executor “有時不是很合適”,它其中有兩個 query 方法,先被執行的方法,對應圖中 CacheExecutor 左側的直線 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler),在方法中它會去調用 MappedStatement#getBoundSql 方法獲取 BoundSql 對象 完成 SQL 的處理和解析,處理和解析后的 BoundSql 對象是我們需要進行攔截處理的,隨后 在該方法內部 調用另一個 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql),對應圖中 CacheExecutor 右側的曲線 query 方法,它會將 BoundSql 作為入參去執行查詢邏輯,結合本次需求,選擇后者切入是合適的,因為它有 BoundSql 入參,對這個入參進行打標即可,我們來看一下 CachingExecutor 的源碼:public class CachingExecutor implements Executor { private final Executor delegate; // 先調用 @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); // 在方法內部調用 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 { // 二級緩存相關邏輯 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) { // 執行查詢邏輯(被攔截) list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list; } } // 執行查詢邏輯(被攔截) return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } } 它使用了靜態代理模式,其中封裝的 Executor 實現類型為 SimpleExecutor,在注釋中標記了“被攔截”處的方法會讓攔截器生效。那么前文中為什么要說它“有時不是很合適”呢?我們來看一種情況,將 Mybatis 配置中的 cacheEnable 置為 false,那么在創建執行器時實際類型不是 CachingExecutor 而是 SimpleExecutor,如下源碼所示:public class Configuration { public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 創建具體的 Executor 實現類 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); } // 攔截器相關邏輯 return (Executor) interceptorChain.pluginAll(executor); } } 當有 SELECT 查詢語句被執行時,它會直接調用到 BaseExecutor#query 方法中,在方法內部調用另一個需要被攔截的 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 { // ... } } 由于該方法是在方法內部被調用的,所以無法使攔截器生效(動態代理),這也是說它“有時不是很合適”的原因所在。因為存在這種情況,我們現在也只能選擇 StatementHandler 作為切入點了,那么是選擇切入 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(); // 創建 StatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 準備 Statement,其中會調用 StatementHandler#prepare 方法 stmt = prepareStatement(handler, ms.getStatementLog()); // 由 StatementHandler 執行 query 方法 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } } 根據源碼,要被執行打標的 BoundSql 對象會在調用 StatementHandler#prepare 方法前會將 BoundSql 對象封裝在 StatementHandler 中,如果選擇切入 StatementHandler#prepare 方法,那么在該方法執行前在 StatementHandler 中拿到 BoundSql 對象進行修改便能實現我們的需求;如果選擇切入 StatementHandler#query 方法,同樣是需要在該方法執行前想辦法獲取到 BoundSql 對象,但是由于此時 SQL 信息已經被保存在了即將與數據庫交互的 Statement 對象中,它的實現類有很多,比如常見的 PreparedStatement,在其中獲取 SQL 字符串相對復雜,所有還是選擇 StatementHandler#prepare 方法作為切點相對容易。攔截器的定義和源碼解析接下來我們來對攔截器進行實現,首先我們先對攔截器的切入點進行定義:@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 { // ... } } 接著來實現其中的邏輯:@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 執行時,StatementHandler 的實際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實際類型的 StatementHandler (靜態代理模式),獲取到實際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關信息,現在對 SQL 信息打標即可 marking(delegate); } } catch (Exception e) { log.error(e.getMessage(), e); } return invocation.proceed(); } } 將自定義的邏輯添加上了 try-catch,避免異常影響正常業務的執行。在主要邏輯中,需要先在 Invocation 中找到 StatementHandler 的實際被代理的對象,它被封裝在了 RoutingStatementHandler 中,隨后在 StatementHandler 中獲取到 BoundSql 對象,對 SQL 進行打標即可(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) { // 可以發現攔截器實際針對的是類型便是 RoutingStatementHandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 攔截器相關邏輯 return (StatementHandler) interceptorChain.pluginAll(statementHandler); } } 我們可以發現攔截器在生效時,針對的是 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; } } 源碼中前兩步為處理代理關系的邏輯,因為 RoutingStatementHandler 可能被代理,需要獲取到實際的被代理對象,找到之后返回即可。那么后續為什么還要獲取到 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) { // 在調用構造方法時,根據 statementType 字段為代理對象 delegate 賦值,那么這樣便實現了復雜度隱藏,只由代理對象去幫忙路由具體的實現即可 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 使用了靜態代理模式,實際的類型被賦值到了 delegate 字段中,我們需要在這個對象中獲取到 BoundSql 對象,獲取 delegate 對象則通過反射來完成。染色打標 marking現在我們已經獲取到了 StatementHandler delegate 對象,我們可以 SQL 進行打標了,但在打標之前我們需要先思考下要打標的內容是什么:要清楚的知道被執行的 SQL 是定義在 Mapper 中的哪條:聲明在 Mapper 中各個方法的唯一ID,也就是 StatementId要清楚的知道這條 SQL 被執行時,有哪些相關方法被執行了:方法的調用棧根據我們所需去找相關的內容就好了,以下是源碼,需要注意的是由于所有類型的 SQL 都會執行到 prepare 方法,但我們只對 SELECT 語句進行打標,所以需要添加邏輯判斷:public class SQLMarkingInterceptor implements Interceptor { private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實際的 SQL String sql = boundSql.getSql().trim(); // 只對 select 打標 if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對象,獲取它的 ID 值表示它是哪條 SQL MappedStatement mappedStatement = getFieldValue( BaseStatementHandler.class, delegate, "mappedStatement", MappedStatement.class ); String mappedStatementId = mappedStatement.getId(); // 方法調用棧 String trace = trace(); // 按順序創建打標的內容 LinkedHashMap markingMap = new LinkedHashMap(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 打標 sql = String.format(" /* %s */ %s", marking, sql); // 反射更新 Field field = getField(BoundSql.class, "sql"); field.set(boundSql, sql); } } } 執行打標的邏輯是修改 BoundSql 對象,將其中的 sql 字段用打標后的 SQL 替換掉。獲取方法調用棧的邏輯我們具體來看一下,其實并不復雜,在全量堆棧信息中將不需要關注的堆棧排除掉,需要注意將 !className.startsWith("com.your.package") 修改成有效的路徑判斷:public class SQLMarkingInterceptor implements Interceptor { private String trace() { // 全量調用棧 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]; // 排除掉不想看到的內容 String className = stackTraceElement.getClassName(); if (!className.startsWith("com.your.package") || className.contains("FastClassBySpringCGLIB") || className.contains("EnhancerBySpringCGLIB") || stackTraceElement.getMethodName().contains("lambda$") ) { continue; } // 過濾攔截器相關 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; } // 格式化結果 StringJoiner stringJoiner = new StringJoiner(" ==> "); for (String method : methodInfoList) { stringJoiner.add(method); } return stringJoiner.toString(); } } 以上便完成了 SQL “染色” 攔截器的實現,將其添加到 mybatis 相關的攔截器配置中就可以生效了。全量源碼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 { /** * 默認線程棧數組下標 */ private static final int DEFAULT_INDEX = 2; /** * 是否開啟SQL染色標記 */ @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 執行時,StatementHandler 的實際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實際類型的 StatementHandler (靜態代理模式),獲取到實際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關信息,現在對 SQL 信息打標即可 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; } /** * 打標 * 1. 要清楚的知道被執行的 SQL 是定義在 Mapper 中的哪條 * 2. 要清楚的知道這條 SQL 被執行時方法的調用棧 */ private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實際的 SQL String sql = boundSql.getSql().trim(); // 只對 select 打標 if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對象,獲取它的 ID 值表示它是哪條 SQL MappedStatement mappedStatement = getFieldValue( BaseStatementHandler.class, delegate, "mappedStatement", MappedStatement.class ); String mappedStatementId = mappedStatement.getId(); // 方法調用棧 String trace = trace(); // 按順序創建打標的內容 LinkedHashMap markingMap = new LinkedHashMap(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 打標 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 { // 獲取到目標類的字段 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; } // 過濾攔截器相關 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; } // 格式化結果 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 織入實現這種方法主要用于在未使用 Mybatis 框架的系統中,基于 AspectJ 實現對 Maven 依賴中 Jar 包類的織入,完成 SQL 染色打標的操作。同時,這種方法并不限于此,大家可以借鑒這種方法用于其他 Jar 包的織入,而不局限于 Spring 提供的 AOP 機制,畢竟 Spring 的 AOP 只能對 Bean 進行織入。所以在本小節中,更注重方法的介紹。添加依賴和配置插件借助 AspectJ 在 編譯期 實現對 Maven 依賴中 Jar 包類的織入,這與運行時織入(如 Spring AOP 使用的代理機制)不同,編譯期織入是在生成的字節碼中直接包含切面邏輯,生成的類文件已經包含了切面代碼。首先,需要先添加依賴: org.aspectj aspectjrt 1.8.13 并且在 Maven 的 plugins 標簽下添加 aspectj-maven-plugin 插件配置,否則無法實現在編譯期織入: org.codehaus.mojo aspectj-maven-plugin 1.11 true ${project.build.directory}/classes 1.8 1.8 1.8 true UTF-8 org.apache.ibatis ibatis-sqlmap compile 解決與 Lombok 的沖突配置內容不再解釋,詳細請看 CSDN: AspectJ和lombok。重點需要關注的配置內容是 weaveDependency 標簽:配置織入依賴(詳細可參見 Maven: aspectj-maven-plugin 官方文檔),也就是說如果我們想對 SqlExecutor 進行織入,那么需要將它對應的 Maven 依賴添加到這個標簽下才能生效,否則無法完成織入。完成以上內容之后,現在去實現對應的攔截器即可。攔截器實現攔截器的實現原理非常簡單,要織入的方法是 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; 根據我們的訴求:在 SQL 執行前對 SQL 進行染色打標,那么可以直接在這個方法的第三個參數 String sql 上打標,以下是攔截器的實現:@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 { // 獲取方法參數 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(); } } // 方法調用棧 String trace = trace(); // 按順序創建打標的內容 LinkedHashMap markingMap = new LinkedHashMap(); markingMap.put("STATEMENT_ID", mappedStatementId); markingMap.put("STACK_TRACE", trace); String marking = "[SQLMarking] ".concat(markingMap.toString()); // 先打標后SQL,避免有些平臺展示SQL時進行尾部截斷,而看不到染色信息 String markingSql = String.format(" /* %s */ %s", marking, sqlTemplate); args[2] = markingSql; } catch (Exception e) { // 發生異常的話恢復最原始 SQL 保證執行 args[2] = sqlTemplate; log.error(e.getMessage(), e); } } // 正常執行邏輯 return joinPoint.proceed(args); } } 邏輯上非常簡單,獲取了 MappedStatementId 和線程的執行堆棧以注釋的形式標記在 SELECT 語句前,注意如果大家要 對 INSERT 語句進行打標時,需要將標記打在 SQL 的最后,因為部分插件如 InsertStatementParser 會識別 INSERT,如果注釋在前,INSERT 識別會有誤報錯。驗證織入完成以上工作后,我們需要驗證攔截器是否織入成功,因為織入是在編譯期完成的,所以執行以下 Maven 編譯命令即可:mvn clean compile 在控制臺中可以發現如下日志信息提示織入成功:[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)) 并且在相應的 target/classes 目錄下的 SqlExecutor.class 文件中也能發現被織入的邏輯: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
    SQL
    +關注

    關注

    1

    文章

    781

    瀏覽量

    44859
  • 數據庫
    +關注

    關注

    7

    文章

    3904

    瀏覽量

    65832
收藏 人收藏

    評論

    相關推薦
    熱點推薦

    oracle數據恢復—oracle數據庫執行錯誤truncate命令如何恢復數據

    oracle數據庫執行truncate命令導致數據丟失是種常見情況。通常情況下,oracle數據庫誤操作刪除
    的頭像 發表于 06-05 16:01 ?158次閱讀
    oracle<b class='flag-5'>數據</b>恢復—oracle<b class='flag-5'>數據庫</b>誤<b class='flag-5'>執行</b>錯誤truncate命令如何恢復<b class='flag-5'>數據</b>?

    不用編程不用聯網,PLC和儀表直接對SQL數據庫,有異常時還可先將數據緩存

    不用PLC編程也不用聯網,還不用電腦,采用IGT-DSER智能網關實現PLC和儀表直接對SQL數據庫。 跟服務端通訊有異常時還可以先將數據暫存,待故障解除后自動重新上報到數據庫;也可
    發表于 04-12 10:47

    如何一眼定位SQL的代碼來源SQL染色標記的簡易MyBatis插件

    作者:京東物流 郭忠強 導語 本文分析了后端研發和運維在日常工作中所面臨的線上SQL定位排查痛點,基于姓名貼的靈感,設計和開發了SQL染色標記的MyBatis插件。該插件輕量高效,
    的頭像 發表于 03-05 11:36 ?363次閱讀
    如何<b class='flag-5'>一眼</b><b class='flag-5'>定位</b><b class='flag-5'>SQL</b>的代碼<b class='flag-5'>來源</b>:<b class='flag-5'>一</b>款<b class='flag-5'>SQL</b>染色標記的簡易MyBatis插件

    數據庫數據恢復—SQL Server附加數據庫提示“錯誤 823”的數據恢復案例

    SQL Server數據庫附加數據庫過程中比較常見的報錯是“錯誤 823”,附加數據庫失敗。 如果數據庫有備份則只需還原備份即可。但是如果
    的頭像 發表于 02-28 11:38 ?422次閱讀
    <b class='flag-5'>數據庫</b><b class='flag-5'>數據</b>恢復—<b class='flag-5'>SQL</b> Server附加<b class='flag-5'>數據庫</b>提示“錯誤 823”的<b class='flag-5'>數據</b>恢復案例

    Devart: dbForge Compare Bundle for SQL Server—比較SQL數據庫最簡單、最準確的方法

    ? dbForge Compare Bundle For SQL Server:包含兩個工具,可幫助您節省用于手動數據庫比較的 70% 的時間 dbForge數據比較 幫助檢測和分析實時SQL
    的頭像 發表于 01-17 11:35 ?483次閱讀

    分布式云化數據庫有哪些類型

    分布式云化數據庫有哪些類型?分布式云化數據庫主要類型包括:關系型分布式數據庫、非關系型分布式數據庫、新SQL分布式
    的頭像 發表于 01-15 09:43 ?420次閱讀

    Oracle數據庫的多功能集成開發環境

    Oracle數據庫的多功能集成開發環境 快捷菜單中的可視化對象編輯器 上下文感知的SQL代碼補全、智能格式化和重構 逐步執行的自動調試功能 多功能數據檢索、存儲和管理
    的頭像 發表于 01-14 13:52 ?348次閱讀
    Oracle<b class='flag-5'>數據庫</b>的多功能集成開發環境

    數據庫是哪種數據庫類型?

    數據庫種部署在虛擬計算環境中的數據庫,它融合了云計算的彈性和可擴展性,為用戶提供高效、靈活的數據庫服務。云數據庫主要分為兩大類:關系型
    的頭像 發表于 01-07 10:22 ?427次閱讀

    不用編程不用電腦,快速實現多臺Modbus協議的PLC、智能儀表對接SQL數據庫

    的參數按照任務組自動生成SQL命令語句,實現多設備SQL命令與數據庫軟件對接,支持MySQL、SQLServer、PostgreSQL、Oracle等。
    的頭像 發表于 12-09 10:53 ?765次閱讀
    不用編程不用電腦,快速實現多臺Modbus協議的PLC、智能儀表對接<b class='flag-5'>SQL</b><b class='flag-5'>數據庫</b>

    SQL數據庫設計的基本原則

    SQL數據庫設計的基本原則 1. 理解需求 在設計數據庫之前,首先要與業務團隊緊密合作,了解業務需求。這包括數據的類型、數據的使用方式、
    的頭像 發表于 11-19 10:23 ?659次閱讀

    SQL與NoSQL的區別

    景。 SQL數據庫 SQL數據庫,也稱為關系型數據庫管理系統(RDBMS),是種基于關系模型的
    的頭像 發表于 11-19 10:15 ?540次閱讀

    數據庫數據恢復—SQL Server數據庫出現823錯誤的數據恢復案例

    SQL Server數據庫故障: SQL Server附加數據庫出現錯誤823,附加數據庫失敗。數據庫
    的頭像 發表于 09-20 11:46 ?645次閱讀
    <b class='flag-5'>數據庫</b><b class='flag-5'>數據</b>恢復—<b class='flag-5'>SQL</b> Server<b class='flag-5'>數據庫</b>出現823錯誤的<b class='flag-5'>數據</b>恢復案例

    數據庫數據恢復—SqlServer數據庫底層File Record被截斷為0的數據恢復案例

    SQL Server數據庫數據無法被讀取。 經過數據庫數據恢復工程師的初步檢測,發現SQL
    的頭像 發表于 07-26 11:27 ?682次閱讀
    <b class='flag-5'>數據庫</b><b class='flag-5'>數據</b>恢復—SqlServer<b class='flag-5'>數據庫</b>底層File Record被截斷為0的<b class='flag-5'>數據</b>恢復案例

    恒訊科技分析:sql數據庫怎么用?

    。 2、安裝數據庫軟件: 在您的服務器或本地計算機上安裝所選的數據庫軟件。 3、配置數據庫服務器: 根據需要配置數據庫服務器設置,包括內存分配、存儲位置、網絡配置等。 4、創建
    的頭像 發表于 07-15 14:40 ?578次閱讀

    數據庫數據恢復—SQL Server數據庫所在分區空間不足報錯的數據恢復案例

    Server數據庫故障: 存放SQL Server數據庫的D盤分區容量不足,管理員在E盤中生成了個.ndf的文件并且將數據庫路徑指向E
    的頭像 發表于 07-10 13:54 ?878次閱讀