背景
在前段時間,我們部門升級了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