日B视频 亚洲,啪啪啪网站一区二区,91色情精品久久,日日噜狠狠色综合久,超碰人妻少妇97在线,999青青视频,亚洲一区二卡,让本一区二区视频,日韩网站推荐

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

大促數(shù)據(jù)庫(kù)壓力激增,如何一眼定位 SQL 執(zhí)行來(lái)源?

京東云 ? 來(lái)源:jf_75140285 ? 作者:jf_75140285 ? 2025-06-10 11:32 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

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

在文章開(kāi)展之前,我們先來(lái)了解一下什么是 SQL 染色:染色的含義是 在 SQL 執(zhí)行前,在 SQL 上進(jìn)行注釋打標(biāo),標(biāo)記內(nèi)容為這條 SQL 對(duì)應(yīng)的是 Mapper 文件中的哪條 SQL 以及相關(guān)的方法執(zhí)行堆棧,如下為在 SGM 的 SQL 執(zhí)行監(jiān)控端能直接看到 SQL 染色信息:

wKgZO2hHp0CAXmO7ABQZuThdHGQ262.png

這樣便能夠非常輕松地看到到底是什么邏輯執(zhí)行了哪段 SQL,并且經(jīng)過(guò)實(shí)際生產(chǎn)性能驗(yàn)證,染色操作耗時(shí)在 0 ~ 1ms 左右:

[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):0ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):0ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):1ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):1ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):1ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):0ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):1ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):0ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):0ms
[JSF-BZ-22000-366-T-20] INFO c.j.b.t.s.SqlExecutorInterceptor [67] - SQL 染色耗時(shí):1ms

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

快速接入 SQL 染色

Mybatis 框架應(yīng)用接入:跳轉(zhuǎn) “全量源碼” 小節(jié),復(fù)制攔截器源碼到應(yīng)用中,并在 Mybatis 攔截器配置中添加該攔截器便可以生效,注意修改源碼中 com.your.package 包路徑為當(dāng)前應(yīng)用的有效包路徑

非 Mybatis 框架應(yīng)用接入:參考 “基于 AspectJ 織入實(shí)現(xiàn)” 小節(jié),通過(guò)對(duì) SQL 執(zhí)行相關(guān) Jar 包進(jìn)行攔截實(shí)現(xiàn)

Mybatis 攔截器實(shí)現(xiàn)

在展示具體實(shí)現(xiàn)前,我還是想通過(guò)給大家介紹原理的形式一步步將其實(shí)現(xiàn),這樣也能加深大家對(duì) Mybatis 框架的理解,也歡迎大家閱讀、訂閱專欄 由 Mybatis 源碼暢談軟件設(shè)計(jì)。如果不想看實(shí)現(xiàn)原理,直接看實(shí)現(xiàn)的話請(qǐng)?zhí)D(zhuǎn) 全量源碼 小節(jié)。

攔截器的作用范圍

Mybatis 的攔截器不像 Spring 的 AOP 機(jī)制,它并不能在任意邏輯處進(jìn)行切入。在 Mybatis 源碼的 Configuration 類中,定義了它的攔截器的作用范圍,即創(chuàng)建“四大處理器”時(shí)調(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 實(shí)現(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 引用不斷變化,會(huì)不斷引用已經(jīng)添加攔截器的對(duì)象
            target = interceptor.plugin(target);
        }
        return target;
    }

    // ...
}

InterceptorChain 實(shí)現(xiàn)非常簡(jiǎn)單,內(nèi)部定義了集合來(lái)保存所有配置的攔截器,執(zhí)行 pluginAll 方法時(shí)會(huì)遍歷該集合,逐個(gè)調(diào)用 Interceptor#plugin 方法來(lái) “不斷地疊加攔截器”(interceptor.plugin 方法執(zhí)行時(shí),target 引用不斷變更)。

注意這里使用到了 責(zé)任鏈模式,由 InterceptorChain 的命名中包含 Chain 也能聯(lián)想到該模式,之后我們?cè)谑褂秘?zé)任鏈時(shí)也可以考慮在命名中增加 Chain 以增加可讀性。InterceptorChain 將多個(gè)攔截器串聯(lián)在一起,每個(gè)攔截器負(fù)責(zé)其特定的邏輯處理,并在執(zhí)行完自己的邏輯后,調(diào)用下一個(gè)攔截器或目標(biāo)方法,這樣設(shè)計(jì)允許不同的攔截器之間的邏輯 解耦,同時(shí)提供了 可擴(kuò)展性。

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

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

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

每個(gè)攔截器切入的 粒度是方法級(jí)別的 的,比如在我們定義的這個(gè)攔截器中,切入的是 StatementHandler#prepare 方法,那么如果我們了解了四個(gè)處理器方法的作用是不是就能知道 Mybatis 攔截器所能實(shí)現(xiàn)的功能了?所以接下來(lái)我們簡(jiǎn)單了解一下它們的各個(gè)方法的作用:

ParameterHandler: 核心方法 setParameters,它的作用主要是將 Java 對(duì)象轉(zhuǎn)換為 SQL 語(yǔ)句中的參數(shù),并處理參數(shù)的設(shè)置和映射,所以攔截器切入它能 對(duì) 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 對(duì)象,攔截器切入它的方法能 對(duì)結(jié)果集進(jìn)行處理

public interface ResultSetHandler {

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

  /**
   * 處理 Statement 對(duì)象并返回一個(gè) Cursor 對(duì)象
   * 它用于處理從數(shù)據(jù)庫(kù)中獲取的大量結(jié)果集,與傳統(tǒng)的 List 或 Collection 不同,Cursor 提供了一種流式處理結(jié)果集的方式,
   * 這在處理大數(shù)據(jù)量時(shí)非常有用,因?yàn)樗梢员苊鈱⑺袛?shù)據(jù)加載到內(nèi)存中
   *
   * @param stmt SQL 語(yǔ)句執(zhí)行后返回的 Statement 對(duì)象
   * @return 游標(biāo)對(duì)象,用于迭代結(jié)果集
   */
   Cursor handleCursorResultSets(Statement stmt) throws SQLException;

  /**
   * 處理存儲(chǔ)過(guò)程的輸出參數(shù)
   *
   * @param cs 存儲(chǔ)過(guò)程調(diào)用的 CallableStatement 對(duì)象
   */
  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

Executor: 它的方法很多,概括來(lái)說(shuō)它負(fù)責(zé)數(shù)據(jù)庫(kù)操作,包括增刪改查等基本的 SQL 操作、管理緩存和事務(wù)的提交與回滾,所以攔截器切入它主要是 管理執(zhí)行過(guò)程或事務(wù)

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

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

    // 該方法用于執(zhí)行查詢操作,接受 `MappedStatement` 對(duì)象(包含 SQL 語(yǔ)句的映射信息)、查詢參數(shù)、分頁(yè)信息、結(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;

    // 該方法用于刷新批處理語(yǔ)句并返回批處理結(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)識(shí)緩存中的唯一查詢結(jié)果
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

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

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

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

    // 該方法用于獲取當(dāng)前的事務(wù)對(duì)象
    Transaction getTransaction();

    // 該方法用于關(guān)閉執(zhí)行器。參數(shù) `forceRollback` 表示是否在關(guān)閉時(shí)強(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í)行階段對(duì) 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();

}

為了加深大家對(duì)這四個(gè)處理器的理解,了解它在查詢 SQL 執(zhí)行時(shí)作用的時(shí)機(jī),我們來(lái)看一下查詢 SQL 執(zhí)行時(shí)的流程圖:

wKgZPGhHp0GAQtYOAAyJ2nAsrkI077.png

每個(gè)聲明 SQL 查詢語(yǔ)句的 Mapper 接口都會(huì)被 MapperProxy 代理,接口中每個(gè)方法都會(huì)被定義為 MapperMethod 對(duì)象,借助 PlainMethodInvoker 執(zhí)行(動(dòng)態(tài)代理模式和策略模式),MapperMethod 中組合了 SqlCommand 和 MethodSignature,SqlCommand 對(duì)象很重要,它的 SqlCommand#name 字段記錄的是 MappedStatement 對(duì)象的 ID 值(eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor),根據(jù)它來(lái)獲取唯一的 MappedStatement(每個(gè) MappedStatement 對(duì)象對(duì)應(yīng) XML 映射文件中一個(gè) , , , 或 標(biāo)簽定義),SqlCommand#type 字段用來(lái)標(biāo)記 SQL 的類型。當(dāng)方法被執(zhí)行時(shí),會(huì)先調(diào)用 SqlSession 中的查詢方法 DefaultSqlSession#selectOne,接著由 執(zhí)行器 Executor 去承接,默認(rèn)類型是 CachingExecutor,注意在這里它會(huì)調(diào)用 MappedStatement#getBoundSql 方法獲取 BoundSql 對(duì)象,這個(gè)對(duì)象實(shí)際上最終都是在 StaticSqlSource#getBoundSql 方法中獲取的,也就是說(shuō) 此時(shí)我們定義在 Mapper 文件中的 SQL 此時(shí)已經(jīng)被解析、處理好了(動(dòng)態(tài)標(biāo)簽等內(nèi)容均已被處理),保存在了 BoundSql 對(duì)象中。此時(shí),要執(zhí)行的 SQL 已經(jīng)準(zhǔn)備好了,它會(huì)接著調(diào)用 SQL 處理器 的 StatementHandler#prepare 方法創(chuàng)建與數(shù)據(jù)庫(kù)交互的 Statement 對(duì)象,其中記錄了要執(zhí)行的 SQL 信息 ,而封裝 SQL 的執(zhí)行參數(shù)則由 參數(shù)處理器 DefaultParameterHandler 和 TypeHandler 完成,ResultSet 結(jié)果的處理:將數(shù)據(jù)庫(kù)中數(shù)據(jù)轉(zhuǎn)換成所需要的 Java 對(duì)象由 結(jié)果處理器 DefaultResultSetHandler 完成。現(xiàn)在我們對(duì)攔截器的原理和查詢 SQL 的執(zhí)行流程已經(jīng)有了基本的了解,回過(guò)頭來(lái)再想一下我們的需求:“使用 Mybatis 的攔截器在 SQL 執(zhí)行前進(jìn)行打標(biāo)”,那么我們?cè)撨x擇哪個(gè)方法作為切入點(diǎn)更合適呢?理論上來(lái)說(shuō)在 Executor, StatementHandler 和 ParameterHandler 相關(guān)的方法中切入都可以,但實(shí)際上我們還要多考慮一步:ParameterHandler 是用來(lái)處理參數(shù)相關(guān)的,在這里切入一般我們是要對(duì)入?yún)?SQL 的入?yún)⑦M(jìn)行處理,所以不選擇這里避免為后續(xù)同學(xué)維護(hù)時(shí)增加理解成本;Executor “有時(shí)不是很合適”,它其中有兩個(gè) query 方法,先被執(zhí)行的方法,對(duì)應(yīng)圖中 CacheExecutor 左側(cè)的直線 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler),在方法中它會(huì)去調(diào)用 MappedStatement#getBoundSql 方法獲取 BoundSql 對(duì)象 完成 SQL 的處理和解析,處理和解析后的 BoundSql 對(duì)象是我們需要進(jìn)行攔截處理的,隨后 在該方法內(nèi)部 調(diào)用另一個(gè) query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql),對(duì)應(yīng)圖中 CacheExecutor 右側(cè)的曲線 query 方法,它會(huì)將 BoundSql 作為入?yún)⑷?zhí)行查詢邏輯,結(jié)合本次需求,選擇后者切入是合適的,因?yàn)樗?BoundSql 入?yún)?,?duì)這個(gè)入?yún)⑦M(jìn)行打標(biāo)即可,我們來(lái)看一下 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 { // 二級(jí)緩存相關(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 實(shí)現(xiàn)類型為 SimpleExecutor,在注釋中標(biāo)記了“被攔截”處的方法會(huì)讓攔截器生效。那么前文中為什么要說(shuō)它“有時(shí)不是很合適”呢?我們來(lái)看一種情況,將 Mybatis 配置中的 cacheEnable 置為 false,那么在創(chuàng)建執(zhí)行器時(shí)實(shí)際類型不是 CachingExecutor 而是 SimpleExecutor,如下源碼所示:public class Configuration { public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 創(chuàng)建具體的 Executor 實(shí)現(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 查詢語(yǔ)句被執(zhí)行時(shí),它會(huì)直接調(diào)用到 BaseExecutor#query 方法中,在方法內(nèi)部調(diào)用另一個(gè)需要被攔截的 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)用的,所以無(wú)法使攔截器生效(動(dòng)態(tài)代理),這也是說(shuō)它“有時(shí)不是很合適”的原因所在。因?yàn)榇嬖谶@種情況,我們現(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,其中會(huì)調(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 對(duì)象會(huì)在調(diào)用 StatementHandler#prepare 方法前會(huì)將 BoundSql 對(duì)象封裝在 StatementHandler 中,如果選擇切入 StatementHandler#prepare 方法,那么在該方法執(zhí)行前在 StatementHandler 中拿到 BoundSql 對(duì)象進(jìn)行修改便能實(shí)現(xiàn)我們的需求;如果選擇切入 StatementHandler#query 方法,同樣是需要在該方法執(zhí)行前想辦法獲取到 BoundSql 對(duì)象,但是由于此時(shí) SQL 信息已經(jīng)被保存在了即將與數(shù)據(jù)庫(kù)交互的 Statement 對(duì)象中,它的實(shí)現(xiàn)類有很多,比如常見(jiàn)的 PreparedStatement,在其中獲取 SQL 字符串相對(duì)復(fù)雜,所有還是選擇 StatementHandler#prepare 方法作為切點(diǎn)相對(duì)容易。攔截器的定義和源碼解析接下來(lái)我們來(lái)對(duì)攔截器進(jìn)行實(shí)現(xiàn),首先我們先對(duì)攔截器的切入點(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 { // ... } } 接著來(lái)實(shí)現(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í)行時(shí),StatementHandler 的實(shí)際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實(shí)際類型的 StatementHandler (靜態(tài)代理模式),獲取到實(shí)際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關(guān)信息,現(xiàn)在對(duì) SQL 信息打標(biāo)即可 marking(delegate); } } catch (Exception e) { log.error(e.getMessage(), e); } return invocation.proceed(); } } 將自定義的邏輯添加上了 try-catch,避免異常影響正常業(yè)務(wù)的執(zhí)行。在主要邏輯中,需要先在 Invocation 中找到 StatementHandler 的實(shí)際被代理的對(duì)象,它被封裝在了 RoutingStatementHandler 中,隨后在 StatementHandler 中獲取到 BoundSql 對(duì)象,對(duì) 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)攔截器實(shí)際針對(duì)的是類型便是 RoutingStatementHandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 攔截器相關(guān)邏輯 return (StatementHandler) interceptorChain.pluginAll(statementHandler); } } 我們可以發(fā)現(xiàn)攔截器在生效時(shí),針對(duì)的是 RoutingStatementHandler 類型,所以我們要獲取該類型,如下源碼:public class SQLMarkingInterceptor implements Interceptor { private RoutingStatementHandler getRoutingStatementHandler(Object target) throws NoSuchFieldException, IllegalAccessException { // 如果被代理,那么一直找到具體被代理的對(duì)象 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)系的邏輯,因?yàn)?RoutingStatementHandler 可能被代理,需要獲取到實(shí)際的被代理對(duì)象,找到之后返回即可。那么后續(xù)為什么還要獲取到 RoutingStatementHandler 中的被代理對(duì)象呢?我們還需要再回到 Mybatis 的源碼中:public class RoutingStatementHandler implements StatementHandler { // 代理對(duì)象 private final StatementHandler delegate; public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 在調(diào)用構(gòu)造方法時(shí),根據(jù) statementType 字段為代理對(duì)象 delegate 賦值,那么這樣便實(shí)現(xiàn)了復(fù)雜度隱藏,只由代理對(duì)象去幫忙路由具體的實(shí)現(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)代理模式,實(shí)際的類型被賦值到了 delegate 字段中,我們需要在這個(gè)對(duì)象中獲取到 BoundSql 對(duì)象,獲取 delegate 對(duì)象則通過(guò)反射來(lái)完成。染色打標(biāo) marking現(xiàn)在我們已經(jīng)獲取到了 StatementHandler delegate 對(duì)象,我們可以 SQL 進(jìn)行打標(biāo)了,但在打標(biāo)之前我們需要先思考下要打標(biāo)的內(nèi)容是什么:要清楚的知道被執(zhí)行的 SQL 是定義在 Mapper 中的哪條:聲明在 Mapper 中各個(gè)方法的唯一ID,也就是 StatementId要清楚的知道這條 SQL 被執(zhí)行時(shí),有哪些相關(guān)方法被執(zhí)行了:方法的調(diào)用棧根據(jù)我們所需去找相關(guān)的內(nèi)容就好了,以下是源碼,需要注意的是由于所有類型的 SQL 都會(huì)執(zhí)行到 prepare 方法,但我們只對(duì) SELECT 語(yǔ)句進(jìn)行打標(biāo),所以需要添加邏輯判斷:public class SQLMarkingInterceptor implements Interceptor { private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實(shí)際的 SQL String sql = boundSql.getSql().trim(); // 只對(duì) select 打標(biāo) if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對(duì)象,獲取它的 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 對(duì)象,將其中的 sql 字段用打標(biāo)后的 SQL 替換掉。獲取方法調(diào)用棧的邏輯我們具體來(lái)看一下,其實(shí)并不復(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ò)濾攔截器相關(guān) if (className.contains("Interceptor") || className.contains("Aspect")) { continue; } // 只拼接類和方法,不拼接文件名和行號(hào) 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 “染色” 攔截器的實(shí)現(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; /** * 是否開(kāi)啟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í)行時(shí),StatementHandler 的實(shí)際類型為 RoutingStatementHandler) RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget()); if (routingStatementHandler != null) { // 其中 delegate 是實(shí)際類型的 StatementHandler (靜態(tài)代理模式),獲取到實(shí)際的 StatementHandler StatementHandler delegate = getFieldValue( RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class ); // 2. 找到 StatementHandler 之后便能拿到 SQL 相關(guān)信息,現(xiàn)在對(duì) SQL 信息打標(biāo)即可 marking(delegate); } } catch (Exception e) { log.error(e.getMessage(), e); } return invocation.proceed(); } private RoutingStatementHandler getRoutingStatementHandler(Object target) throws NoSuchFieldException, IllegalAccessException { // 如果被代理,那么一直找到具體被代理的對(duì)象 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í)行時(shí)方法的調(diào)用棧 */ private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException { BoundSql boundSql = delegate.getBoundSql(); // 實(shí)際的 SQL String sql = boundSql.getSql().trim(); // 只對(duì) select 打標(biāo) if (StringUtils.containsIgnoreCase(sql, "select")) { // 獲取其基類中的 MappedStatement 即定義的 SQL 聲明對(duì)象,獲取它的 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 某對(duì)象 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ò)濾攔截器相關(guān) if (className.contains("Interceptor") || className.contains("Aspect")) { continue; } // 只拼接類和方法,不拼接文件名和行號(hào) 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 織入實(shí)現(xiàn)這種方法主要用于在未使用 Mybatis 框架的系統(tǒng)中,基于 AspectJ 實(shí)現(xiàn)對(duì) Maven 依賴中 Jar 包類的織入,完成 SQL 染色打標(biāo)的操作。同時(shí),這種方法并不限于此,大家可以借鑒這種方法用于其他 Jar 包的織入,而不局限于 Spring 提供的 AOP 機(jī)制,畢竟 Spring 的 AOP 只能對(duì) Bean 進(jìn)行織入。所以在本小節(jié)中,更注重方法的介紹。添加依賴和配置插件借助 AspectJ 在 編譯期 實(shí)現(xiàn)對(duì) Maven 依賴中 Jar 包類的織入,這與運(yùn)行時(shí)織入(如 Spring AOP 使用的代理機(jī)制)不同,編譯期織入是在生成的字節(jié)碼中直接包含切面邏輯,生成的類文件已經(jīng)包含了切面代碼。首先,需要先添加依賴: org.aspectj aspectjrt 1.8.13 并且在 Maven 的 plugins 標(biāo)簽下添加 aspectj-maven-plugin 插件配置,否則無(wú)法實(shí)現(xiàn)在編譯期織入: 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 的沖突配置內(nèi)容不再解釋,詳細(xì)請(qǐng)看 CSDN: AspectJ和lombok。重點(diǎn)需要關(guān)注的配置內(nèi)容是 weaveDependency 標(biāo)簽:配置織入依賴(詳細(xì)可參見(jiàn) Maven: aspectj-maven-plugin 官方文檔),也就是說(shuō)如果我們想對(duì) SqlExecutor 進(jìn)行織入,那么需要將它對(duì)應(yīng)的 Maven 依賴添加到這個(gè)標(biāo)簽下才能生效,否則無(wú)法完成織入。完成以上內(nèi)容之后,現(xiàn)在去實(shí)現(xiàn)對(duì)應(yīng)的攔截器即可。攔截器實(shí)現(xiàn)攔截器的實(shí)現(xiàn)原理非常簡(jiǎn)單,要織入的方法是 com.ibatis.sqlmap.engine.execution.SqlExecutor#executeQuery,這個(gè)方法的簽名如下:public void executeQuery(StatementScope statementScope, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException; 根據(jù)我們的訴求:在 SQL 執(zhí)行前對(duì) SQL 進(jìn)行染色打標(biāo),那么可以直接在這個(gè)方法的第三個(gè)參數(shù) String sql 上打標(biāo),以下是攔截器的實(shí)現(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) { // 實(shí)際的 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,避免有些平臺(tái)展示SQL時(shí)進(jìn)行尾部截?cái)?,而看不到染色信?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); } } 邏輯上非常簡(jiǎn)單,獲取了 MappedStatementId 和線程的執(zhí)行堆棧以注釋的形式標(biāo)記在 SELECT 語(yǔ)句前,注意如果大家要 對(duì) INSERT 語(yǔ)句進(jìn)行打標(biāo)時(shí),需要將標(biāo)記打在 SQL 的最后,因?yàn)椴糠植寮?InsertStatementParser 會(huì)識(shí)別 INSERT,如果注釋在前,INSERT 識(shí)別會(huì)有誤報(bào)錯(cuò)。驗(yàn)證織入完成以上工作后,我們需要驗(yàn)證攔截器是否織入成功,因?yàn)榭椚胧窃诰幾g期完成的,所以執(zhí)行以下 Maven 編譯命令即可:mvn clean compile 在控制臺(tái)中可以發(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)); } } 以上,大功告成。


審核編輯 黃宇

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • SQL
    SQL
    +關(guān)注

    關(guān)注

    1

    文章

    807

    瀏覽量

    46962
  • 數(shù)據(jù)庫(kù)
    +關(guān)注

    關(guān)注

    7

    文章

    4085

    瀏覽量

    68567
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    MySQL數(shù)據(jù)庫(kù)慢查詢分析與優(yōu)化實(shí)戰(zhàn)

    在討論MySQL慢查詢之前,需要先明確個(gè)關(guān)鍵前提:什么是慢查詢? 不同業(yè)務(wù)場(chǎng)景下,慢查詢的定義差異巨大。個(gè)數(shù)據(jù)報(bào)表后臺(tái)的SQL執(zhí)行30秒
    的頭像 發(fā)表于 04-02 09:38 ?187次閱讀

    MySQL 慢 SQL 排查這件事,NineData 社區(qū)VS DBeaver/ Navicat 技術(shù)分析

    社區(qū)版的定位不同,它是免費(fèi)、本地化部署的數(shù)據(jù)管理平臺(tái),將數(shù)據(jù)庫(kù) DevOps、數(shù)據(jù)復(fù)制、數(shù)據(jù)庫(kù)對(duì)比三大能力整合于
    的頭像 發(fā)表于 03-17 11:53 ?146次閱讀
    MySQL 慢 <b class='flag-5'>SQL</b> 排查這件事,NineData 社區(qū)VS DBeaver/ Navicat 技術(shù)分析

    恒訊科技解析:如何安裝MySQL并創(chuàng)建數(shù)據(jù)庫(kù)

    管理系統(tǒng)(RDBMS),使用結(jié)構(gòu)化查詢語(yǔ)言(SQL)高效地組織和管理數(shù)據(jù)。它是全球最受歡迎的開(kāi)源數(shù)據(jù)庫(kù)系統(tǒng)之,廣泛應(yīng)用于網(wǎng)頁(yè)開(kāi)發(fā)、電子商務(wù)和商業(yè)應(yīng)用。 常見(jiàn)用例? MySQL 是多種
    的頭像 發(fā)表于 01-14 14:25 ?398次閱讀

    不用編程不用聯(lián)網(wǎng),實(shí)現(xiàn)倍福(BECKHOFF)PLC對(duì)接SQL數(shù)據(jù)庫(kù),上報(bào)和查詢數(shù)據(jù)的案例

    消息區(qū)域顯示的狀態(tài)消息;根據(jù)狀態(tài)消息調(diào)試。如下圖:智能網(wǎng)關(guān)根據(jù)以上配置自動(dòng)生成SQL語(yǔ)句命令,結(jié)合數(shù)據(jù)庫(kù)相關(guān)參數(shù)對(duì)接到服務(wù)器,多個(gè)SQL語(yǔ)句用\'任務(wù)組\'區(qū)分,相同的任務(wù)組即為同個(gè)
    發(fā)表于 10-10 11:14

    mysql數(shù)據(jù)恢復(fù)—mysql數(shù)據(jù)庫(kù)表被truncate的數(shù)據(jù)恢復(fù)案例

    某云ECS網(wǎng)站服務(wù)器,linux操作系統(tǒng),部署了mysql數(shù)據(jù)庫(kù)。工作人員在執(zhí)行數(shù)據(jù)庫(kù)版本更新測(cè)試時(shí),錯(cuò)誤地將本應(yīng)在測(cè)試庫(kù)執(zhí)行sql腳本在
    的頭像 發(fā)表于 09-11 09:28 ?1222次閱讀
    mysql<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)—mysql<b class='flag-5'>數(shù)據(jù)庫(kù)</b>表被truncate的<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)案例

    數(shù)據(jù)庫(kù)慢查詢分析與SQL優(yōu)化實(shí)戰(zhàn)技巧

    今天,我將分享我在處理數(shù)千次數(shù)據(jù)庫(kù)性能問(wèn)題中積累的實(shí)戰(zhàn)經(jīng)驗(yàn),幫助你系統(tǒng)掌握慢查詢分析與SQL優(yōu)化的核心技巧。無(wú)論你是剛?cè)腴T的運(yùn)維新手,還是有定經(jīng)驗(yàn)的工程師,這篇文章都將為你提供實(shí)用的解決方案。
    的頭像 發(fā)表于 09-08 09:34 ?1266次閱讀

    數(shù)據(jù)庫(kù)性能優(yōu)化指南

    作為名在大廠摸爬滾打多年的運(yùn)維老兵,我見(jiàn)過(guò)太多因?yàn)?b class='flag-5'>數(shù)據(jù)庫(kù)性能問(wèn)題導(dǎo)致的生產(chǎn)事故。今天分享套完整的數(shù)據(jù)庫(kù)優(yōu)化方法論,從SQL層面到硬件配置
    的頭像 發(fā)表于 08-18 11:21 ?917次閱讀

    不用編程序無(wú)需聯(lián)外網(wǎng),將Rockwell羅克韋爾(AB)PLC的標(biāo)簽數(shù)據(jù)存入SQL數(shù)據(jù)庫(kù)

    IGT-DSER智能網(wǎng)關(guān)模塊,支持各種PLC、智能儀表、遠(yuǎn)程IO與數(shù)據(jù)庫(kù)之間雙向通訊,既可以讀取設(shè)備的數(shù)據(jù)上報(bào)到SQL數(shù)據(jù)庫(kù),也可以從數(shù)據(jù)庫(kù)
    發(fā)表于 07-31 10:33

    數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)—服務(wù)器異常斷電導(dǎo)致Oracle數(shù)據(jù)庫(kù)故障的數(shù)據(jù)恢復(fù)案例

    Oracle數(shù)據(jù)庫(kù)故障: 某公司臺(tái)服務(wù)器上部署Oracle數(shù)據(jù)庫(kù)。服務(wù)器意外斷電導(dǎo)致數(shù)據(jù)庫(kù)報(bào)錯(cuò),報(bào)錯(cuò)內(nèi)容為“system01.dbf需要更多的恢復(fù)來(lái)保持
    的頭像 發(fā)表于 07-24 11:12 ?869次閱讀
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>恢復(fù)—服務(wù)器異常斷電導(dǎo)致Oracle<b class='flag-5'>數(shù)據(jù)庫(kù)</b>故障的<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)案例

    數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)—MongoDB數(shù)據(jù)庫(kù)文件丟失的數(shù)據(jù)恢復(fù)案例

    MongoDB數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)環(huán)境: 臺(tái)操作系統(tǒng)為Windows Server的虛擬機(jī)上部署MongoDB數(shù)據(jù)庫(kù)。 MongoDB數(shù)據(jù)庫(kù)
    的頭像 發(fā)表于 07-01 11:13 ?809次閱讀
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>恢復(fù)—MongoDB<b class='flag-5'>數(shù)據(jù)庫(kù)</b>文件丟失的<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)案例

    數(shù)據(jù)庫(kù)數(shù)據(jù)恢復(fù)—SQL Server數(shù)據(jù)庫(kù)被加密如何恢復(fù)數(shù)據(jù)

    SQL Server數(shù)據(jù)庫(kù)故障: SQL Server數(shù)據(jù)庫(kù)被加密,無(wú)法使用。 數(shù)據(jù)庫(kù)MDF、LDF、log日志文件名字被篡改。
    的頭像 發(fā)表于 06-25 13:54 ?866次閱讀
    <b class='flag-5'>數(shù)據(jù)庫(kù)</b><b class='flag-5'>數(shù)據(jù)</b>恢復(fù)—<b class='flag-5'>SQL</b> Server<b class='flag-5'>數(shù)據(jù)庫(kù)</b>被加密如何恢復(fù)<b class='flag-5'>數(shù)據(jù)</b>?

    達(dá)夢(mèng)數(shù)據(jù)庫(kù)常用管理SQL命令詳解

    達(dá)夢(mèng)數(shù)據(jù)庫(kù)常用管理SQL命令詳解
    的頭像 發(fā)表于 06-17 15:12 ?7686次閱讀
    達(dá)夢(mèng)<b class='flag-5'>數(shù)據(jù)庫(kù)</b>常用管理<b class='flag-5'>SQL</b>命令詳解

    oracle數(shù)據(jù)恢復(fù)—oracle數(shù)據(jù)庫(kù)執(zhí)行錯(cuò)誤truncate命令如何恢復(fù)數(shù)據(jù)?

    oracle數(shù)據(jù)庫(kù)執(zhí)行truncate命令導(dǎo)致數(shù)據(jù)丟失是種常見(jiàn)情況。通常情況下,oracle數(shù)據(jù)庫(kù)誤操作刪除
    的頭像 發(fā)表于 06-05 16:01 ?1851次閱讀
    oracle<b class='flag-5'>數(shù)據(jù)</b>恢復(fù)—oracle<b class='flag-5'>數(shù)據(jù)庫(kù)</b>誤<b class='flag-5'>執(zhí)行</b>錯(cuò)誤truncate命令如何恢復(fù)<b class='flag-5'>數(shù)據(jù)</b>?

    SQLSERVER數(shù)據(jù)庫(kù)是什么

    SQL Server 是由微軟公司開(kāi)發(fā)的款 關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng)(RDBMS) ,用于存儲(chǔ)、管理和檢索結(jié)構(gòu)化數(shù)據(jù)。它是企業(yè)級(jí)應(yīng)用中廣泛使用的數(shù)據(jù)庫(kù)
    的頭像 發(fā)表于 05-26 09:19 ?1315次閱讀

    MySQL數(shù)據(jù)庫(kù)是什么

    MySQL數(shù)據(jù)庫(kù)種 開(kāi)源的關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng)(RDBMS) ,由瑞典MySQL AB公司開(kāi)發(fā),后被Oracle公司收購(gòu)。它通過(guò)結(jié)構(gòu)化查詢語(yǔ)言(SQL)進(jìn)行
    的頭像 發(fā)表于 05-23 09:18 ?1471次閱讀
    江阴市| 连州市| 祁门县| 南昌市| 古田县| 布尔津县| 佳木斯市| 若尔盖县| 舟山市| 平和县| 井冈山市| 太仆寺旗| 鹿邑县| 株洲市| 巴东县| 石门县| 正定县| 通州区| 正阳县| 宁河县| 青神县| 石台县| 庆阳市| 繁峙县| 四川省| 崇文区| 会东县| 克东县| 兰西县| 元谋县| 色达县| 安阳县| 额济纳旗| 承德县| 晋江市| 肇庆市| 蒙自县| 雷波县| 运城市| 迁西县| 奉新县|