基于 Javassist 和 Javaagent 实现Sql打印

背景

在前段时间,我们部门升级了mybati-plus(以下简称mp)的版本,官方在新版的mp中去掉了性能监控的intercept,导致无法像以前一样进行打印完整的sql。mp官方说是可以使用p6spy解决,但是这个需要在项目中引入额外的jar包,开发随便引入额外的jar包可能会出现意想不到的问题(主要是咱也做不了主)。而mybatis原生的sql日志,在遇到问题想要获取到sql时非常麻烦,特别是参数较多的情况下。于是就在思考有没有一种技术既可以简单的获取我想要的sql语句呢。经过研究发现可以利用JavaAgent技术和javassist字节码插装技术,可以做到无侵入式的打印完整的sql。

技术简介

  • JavaAgent

JavaAgent相当于一个插件,在JVM启动的时候可以添加 JavaAgent配置指定启动之前需要启动的agent jar包,例如 java –javaagent:myagent.jar –jar main.jar。

这样在程序启动的时候会去执行myagent.jar包中MANIFEST.MF文件指定的类中的premain方法。Javaagent可以分为两种,上面提到的是其中一种,在主程序之前运行的Agent,另一种则是在JDK1.6之后提供的主程序之后运行的Agent,MANIFEST.MF文件指定的类中的agentmain方法(前者是JDK1.5提供的)。

  • Javassist

Javassist是可以动态编辑Java字节码的类库。例如,可以在java程序运行过程中用代码写一个新的类,并加载到jvm中使用;可以在类加载过程中对类进行修改(这里我们就是用到这个特性)

使用流程如下:


使用流程图

需求分析

想要获取完整额sql,可以从orm框架入手,但是orm本身就兼容很多数据库,复杂度会比较高,需要实现的细节也比较多,市面上能说出来的就有hibernate,mybatis,jdbctemplate(这个好像不算orm),spring data jpa(基于hibernate)等等。所以引出了一个问题,如果项目里面没用orm,纯粹用jdbc怎么办呢?

  • 结论
    直接上结论吧,直接从jdbc的驱动下手,经过漫长的研究分析调试代码,发现jdbc里面有三个方法,可以直接获取sql(java.sql.Statement这里直接忽略了)
    java.sql.PreparedStatement#execute
    java.sql.PreparedStatement#executeUpdate
    java.sql.PreparedStatement#executeQuery

实现

想要实现这个功能还是需要每个jdbc驱动去逐个适配的,我这里只实现了postgreDB的,其他数据库可以去跟踪下源码,找到驱动中PreparedStatement的实现类,自己改下就可以了
这里代码量非常少,直接上代码

  • 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));
    }
}

使用

  • 在启动的虚拟机参数中添加参数即可
    -javaagent:D:\sql-agent-1.0-SNAPSHOT-jar-with-dependencies.jar=isFormatSql=false
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容