基于 Javassist 和 Javaagent 實現(xiàn)Sql打印

背景

在前段時間,我們部門升級了mybati-plus(以下簡稱mp)的版本,官方在新版的mp中去掉了性能監(jiān)控的intercept,導(dǎo)致無法像以前一樣進行打印完整的sql珊擂。mp官方說是可以使用p6spy解決圣勒,但是這個需要在項目中引入額外的jar包,開發(fā)隨便引入額外的jar包可能會出現(xiàn)意想不到的問題(主要是咱也做不了主)摧扇。而mybatis原生的sql日志圣贸,在遇到問題想要獲取到sql時非常麻煩,特別是參數(shù)較多的情況下扛稽。于是就在思考有沒有一種技術(shù)既可以簡單的獲取我想要的sql語句呢吁峻。經(jīng)過研究發(fā)現(xiàn)可以利用JavaAgent技術(shù)和javassist字節(jié)碼插裝技術(shù),可以做到無侵入式的打印完整的sql在张。

技術(shù)簡介

  • JavaAgent

JavaAgent相當(dāng)于一個插件锡搜,在JVM啟動的時候可以添加 JavaAgent配置指定啟動之前需要啟動的agent jar包,例如 java –javaagent:myagent.jar –jar main.jar瞧掺。

這樣在程序啟動的時候會去執(zhí)行myagent.jar包中MANIFEST.MF文件指定的類中的premain方法。Javaagent可以分為兩種凡傅,上面提到的是其中一種辟狈,在主程序之前運行的Agent,另一種則是在JDK1.6之后提供的主程序之后運行的Agent夏跷,MANIFEST.MF文件指定的類中的agentmain方法(前者是JDK1.5提供的)哼转。

  • Javassist

Javassist是可以動態(tài)編輯Java字節(jié)碼的類庫。例如槽华,可以在java程序運行過程中用代碼寫一個新的類壹蔓,并加載到j(luò)vm中使用;可以在類加載過程中對類進行修改(這里我們就是用到這個特性)

使用流程如下:


使用流程圖

需求分析

想要獲取完整額sql猫态,可以從orm框架入手佣蓉,但是orm本身就兼容很多數(shù)據(jù)庫,復(fù)雜度會比較高亲雪,需要實現(xiàn)的細(xì)節(jié)也比較多勇凭,市面上能說出來的就有hibernate,mybatis义辕,jdbctemplate(這個好像不算orm)虾标,spring data jpa(基于hibernate)等等。所以引出了一個問題灌砖,如果項目里面沒用orm璧函,純粹用jdbc怎么辦呢?

  • 結(jié)論
    直接上結(jié)論吧基显,直接從jdbc的驅(qū)動下手蘸吓,經(jīng)過漫長的研究分析調(diào)試代碼,發(fā)現(xiàn)jdbc里面有三個方法撩幽,可以直接獲取sql(java.sql.Statement這里直接忽略了)
    java.sql.PreparedStatement#execute
    java.sql.PreparedStatement#executeUpdate
    java.sql.PreparedStatement#executeQuery

實現(xiàn)

想要實現(xiàn)這個功能還是需要每個jdbc驅(qū)動去逐個適配的美澳,我這里只實現(xiàn)了postgreDB的,其他數(shù)據(jù)庫可以去跟蹤下源碼,找到驅(qū)動中PreparedStatement的實現(xiàn)類制跟,自己改下就可以了
這里代碼量非常少舅桩,直接上代碼

  • pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>xyz.dava</groupId>
    <artifactId>sql-agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
        <!--   偷個懶,sql美化雨膨,不是必須     -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>xyz.dava.agent.sql.Main</Premain-Class>
                            <Agent-Class>xyz.dava.agent.sql.Main</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  • 代碼
package xyz.dava.agent.sql;

import com.alibaba.druid.sql.SQLUtils;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * Main
 *
 * @author dava
 * @date 2022/01/19 11:21
 * @description
 * @since 1.0.0
 */
public class Main {

    private static ClassPool classPool;


    public static void premain(String args, Instrumentation instrumentation) {
        boolean isFormatSql = args.contains("isFormatSql=true");
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader,
                                    String className,
                                    Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain,
                                    byte[] classfileBuffer) throws IllegalClassFormatException {
                if (!"org/postgresql/jdbc/PgPreparedStatement".equals(className)) {
                    return null;
                }
                try {
                    classPool = ClassPool.getDefault();
                    classPool.appendClassPath(new LoaderClassPath(loader));

                    CtClass ctClass = classPool.get("org.postgresql.jdbc.PgPreparedStatement");
                    ctClass.addMethod(getPrintSqlMethod(ctClass));
                    CtMethod m1 = ctClass.getDeclaredMethod("execute", new CtClass[]{});
                    m1.insertBefore("{davaPrintSql(preparedQuery.query.toString(preparedParameters)," + isFormatSql + ");}");
                    CtMethod m2 = ctClass.getDeclaredMethod("executeQuery", new CtClass[]{});
                    m2.insertBefore("{davaPrintSql(preparedQuery.query.toString(preparedParameters)," + isFormatSql + ");}");
                    CtMethod m3 = ctClass.getDeclaredMethod("executeUpdate", new CtClass[]{});
                    m3.insertBefore("{davaPrintSql(preparedQuery.query.toString(preparedParameters)," + isFormatSql + ");}");
                    return ctClass.toBytecode();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
    }

    public static CtMethod getPrintSqlMethod(CtClass cls) throws Exception {
        CtMethod originMethod = classPool.getMethod("xyz.dava.agent.sql.Main", "dataPrintSql");
        CtMethod method = CtNewMethod.copy(originMethod, cls, null);
        method.setName("davaPrintSql");
        return method;
    }

    public void dataPrintSql(String sql, boolean isFormatSql) {
        SQLUtils.FormatOption option = new SQLUtils.FormatOption();
        option.setPrettyFormat(isFormatSql);
        System.err.println(SQLUtils.formatPGSql(sql, option));
    }
}

使用

  • 在啟動的虛擬機參數(shù)中添加參數(shù)即可
    -javaagent:D:\sql-agent-1.0-SNAPSHOT-jar-with-dependencies.jar=isFormatSql=false
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末擂涛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子聊记,更是在濱河造成了極大的恐慌撒妈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件排监,死亡現(xiàn)場離奇詭異狰右,居然都是意外死亡,警方通過查閱死者的電腦和手機舆床,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門棋蚌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挨队,你說我怎么就攤上這事谷暮。” “怎么了盛垦?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵湿弦,是天一觀的道長。 經(jīng)常有香客問我腾夯,道長颊埃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任蝶俱,我火速辦了婚禮竟秫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跷乐。我一直安慰自己肥败,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布愕提。 她就那樣靜靜地躺著馒稍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浅侨。 梳的紋絲不亂的頭發(fā)上纽谒,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音如输,去河邊找鬼鼓黔。 笑死央勒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的澳化。 我是一名探鬼主播崔步,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缎谷!你這毒婦竟也來了井濒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤列林,失蹤者是張志新(化名)和其女友劉穎瑞你,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體希痴,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡者甲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了砌创。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虏缸。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖纺铭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情刀疙,我是刑警寧澤舶赔,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站谦秧,受9級特大地震影響竟纳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疚鲤,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一锥累、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧集歇,春花似錦桶略、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姑蓝,卻和暖如春鹅心,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纺荧。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工旭愧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留颅筋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓输枯,卻偏偏與公主長得像议泵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子用押,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容