你是否有在使用 IntelliJ IDEA 做開發(fā)的過程,需要拿到執(zhí)行 SQL 語句,復(fù)制出來做驗證的時候,總是這樣的語句:SELECT * FROM USER WHERE id = ? AND name = ? 又需要自己把 ? 號 替換成入?yún)⒅的兀?/div>
一、前言
片面了!
一月三舟,托爾斯泰說:“多么偉大的作家,也不過就是在書寫自己的片面而已”。何況是我,何況是我們!
雖然我們不書寫文章,但我們寫需求、寫代碼、寫注釋,當(dāng)我們遇到了需要被討論的問題點時,往往變成了爭論點。這個好、那個差、你用的都是啥啥啥!
當(dāng)你把路走窄了,你所能接受到的新的思路、新的想法、新的視野,以及非常重要的收入,也都會隨之減少。只有橫向?qū)Ρ取⒖冀梃b、查漏補缺,才能讓你的頭腦中會有更多的思路,無論是在寫代碼上、還是在理財上、還是在生活上。
二、需求目的
你是否有在使用 IntelliJ IDEA 做開發(fā)的過程,需要拿到執(zhí)行 SQL 語句,復(fù)制出來做驗證的時候,總是這樣的語句:SELECT * FROM USER WHERE id = ? AND name = ? 又需要自己把 ? 號 替換成入?yún)⒅的兀?/p>
當(dāng)然這個需求其實并不大,甚至你還可以使用其他方式解決。那么在本章節(jié)會給你提供一個新的思路,可能你幾乎是沒過的方式進(jìn)行處理。
那么在這個章節(jié)的案例中我們用到基于 IDEA Plugin 開發(fā)能力,把字節(jié)碼插樁探針,基于 Javaagent 的能力,注入到代碼中。再通過增強(qiáng)后的字節(jié)碼,獲取到 com.mysql.jdbc.PreparedStatement -> executeInternal 執(zhí)行時的對象,從而拿到可以直接測試的 SQL 語句。
三、案例開發(fā)
1. 工程結(jié)構(gòu)
guide-idea-plugin-probe
├── .gradle
├── probe-agent
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn.bugstack.guide.idea.plugin
│ │ ├── MonitorMethod.java
│ │ └── PreAgent.java
│ └── build.gradle
└── probe-plugin
│ └── src
│ │ └── main
│ │ ├── java
│ │ │ └── cn.bugstack.guide.idea.plugin
│ │ │ └── utils
│ │ │ │ └── PluginUtil.java
│ │ │ └── PerRun.java
│ │ └── resources
│ │ └── META-INF
│ │ └── plugin.xml
│ └── build.gradle
├── build.gradle
└── gradle.properties
公眾號:bugstack蟲洞棧 回復(fù):idea 即可下載全部 IDEA 插件開發(fā)源碼
在此 IDEA 插件工程中,工程結(jié)構(gòu)分為2塊:
- probe-agent:探針模塊,用于編譯打包提供字節(jié)碼增強(qiáng)服務(wù),給 probe-plugin 模塊使用
- probe-plugin:插件模塊,通過 java.programPatcher加載字節(jié)碼增強(qiáng)包,獲取并打印執(zhí)行數(shù)據(jù)庫操作的 SQL 語句。
2. 字節(jié)碼增強(qiáng)獲取 SQL
此處的字節(jié)碼增強(qiáng)方式,采用的 Byte-Buddy 字節(jié)碼框架,它的使用方式更加簡單,在使用的過程中有些像使用 AOP 的攔截方式一樣,獲取到你需要的信息。
此外在 gradle 打包構(gòu)建的時候,需要添加 shadowJar 模塊,把 Premain-Class 打包進(jìn)去。這部分代碼中可以查看
2.1 探針入口
cn.bugstack.guide.idea.plugin.PreAgent
//JVM 首先嘗試在代理類上調(diào)用以下方法
public static void premain(String agentArgs, Instrumentation inst){
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
return builder
.method(ElementMatchers.named("executeInternal")) // 攔截任意方法
.intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
};
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith("com.mysql.jdbc.PreparedStatement"))
.transform(transformer)
.installOn(inst);
}
- 通過 Byte-buddy 配置,攔截匹配的類和方法,因為這個類和方法下,可以獲取到完整的執(zhí)行 SQL 語句。
2.2 攔截 SQL
cn.bugstack.guide.idea.plugin.MonitorMethod
@RuntimeType
public static Object intercept(@This Object obj, @Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object... args) throws Exception {
try {
return callable.call();
} finally {
String originalSql = (String) BeanUtil.getFieldValue(obj, "originalSql");
String replaceSql = ReflectUtil.invoke(obj, "asSql");
System.out.println("數(shù)據(jù)庫名稱:Mysql");
System.out.println("線程ID:" + Thread.currentThread().getId());
System.out.println("時間:" + new Date());
System.out.println("原始SQL:\r\n" + originalSql);
System.out.println("替換SQL:\r\n" + replaceSql);
}
}
- 攔截方法入?yún)⑹且环N可配置操作,比如 @This Object obj是為了獲取當(dāng)前類的執(zhí)行對象,@Origin Method method是為了獲取執(zhí)行方法。
- 在 finally 塊中,我們可以通過反射拿到當(dāng)前類的屬性信息,以及反射拿到執(zhí)行的 SQL,并做打印輸出。
2.3 編譯打包
在測試和開發(fā) IDEA Plugin 插件之前,我們需要先進(jìn)行一個打包操作,這個打包就是把字節(jié)碼增強(qiáng)的代碼打包整一個 Jar 包。在 build.gradle -> shadowJar

- 打包編譯后,就可以在 build -> libs 下看到 Jar:probe-agent-1.0-SNAPSHOT-all.jar這個 Jar 就是用來做字節(jié)碼增強(qiáng)處理的。
2.4 測試驗證
這里在把寫好的字節(jié)碼增強(qiáng)組件給插件使用之前,可以做一個測試驗證,避免每次都需要啟動插件才能做測試。
單元測試
public class ApiTest {
public static void main(String[] args) throws Exception {
String URL = "jdbc:mysql://127.0.0.1:3306/itstack?characterEncoding=utf-8";
String USER = "root";
String PASSWORD = "123456";
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
String sql="SELECT * FROM USER WHERE id = ? AND name = ?";
PreparedStatement statement = conn.prepareStatement(sql);
statement.setLong(1,1L);
statement.setString(2,"謝飛機(jī)");
ResultSet rs = statement.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name") + " " + rs.getString("address"));
}
}
}
- VM options:-javaagent:你的路徑\libs\probe-agent-1.0-SNAPSHOT-all.jar
- 注意在測試運行的時候,你要給 ApiTest 配置 VM options 才能打印攔截 SQL 信息
測試結(jié)果
原始SQL:
SELECT * FROM USER WHERE id = ? AND name = ?
替換SQL:
SELECT * FROM USER WHERE id = 1 AND name = '謝飛機(jī)'
謝飛機(jī) 北京.大興區(qū).通明湖公園
- 好啦,這樣我們就可以攔截可以復(fù)制執(zhí)行的 SQL 語句了,接下來我們再做下 IDEA Plugin 的處理。
3. 通過插件開發(fā)引入探針 Jar
接下來我們要把開發(fā)好的字節(jié)碼增強(qiáng) Jar 包,復(fù)制到 IDEA Plugin 插件開發(fā)模塊中的 libs(可自己創(chuàng)建) 下,之后在 plugin.xml 配置加載 implementation fileTree(dir: 'libs', includes: ['*jar']) 這樣就可以程序中,找到這個 jar 包并配置到程序中。
3.1 復(fù)制 jar 到 libs 下

3.2 build.gradle 配置加載
dependencies {
implementation fileTree(dir: 'libs', includes: ['*jar'])
}
- 通過 implementation fileTree引入加載文件樹的方式,把我們配置好的 Jar 加載到程序運行中。
3.3 程序中引入 javaagent
cn.bugstack.guide.idea.plugin.PerRun
public class PerRun extends JavaProgramPatcher {
@Override
public void patchJavaParameters(Executor executor, RunProfile configuration, JavaParameters javaParameters){
RunConfiguration runConfiguration = (RunConfiguration) configuration;
ParametersList vmParametersList = javaParameters.getVMParametersList();
vmParametersList.addParametersString("-javaagent:" + agentCoreJarPath);
vmParametersList.addNotEmptyProperty("guide-idea-plugin-probe.projectId", runConfiguration.getProject().getLocationHash());
}
}
- 通過繼承 JavaProgramPatcher類,實現(xiàn)patchJavaParameters方法,通過 configuration 屬性來配置我們自己需要被加載的-javaagent包。
- 這樣在通過 IDEA 已經(jīng)安裝此插件,運行代碼的時候,就會執(zhí)行到這個攔截和打印 SQL 的功能。
3.4 plugin.xml 添加配置
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<java.programPatcher implementation="cn.bugstack.guide.idea.plugin.PerRun"/>
</extensions>
- 之后你還需要把開發(fā)好的加載類,配置到 java.programPatcher這樣就可以程序運行的時候,被加載到了。
四、測試驗證
- 準(zhǔn)備好一個有數(shù)據(jù)庫操作的工程,需要的是 JDBC,如果是其他的,你需要自己擴(kuò)展
- 啟動插件后,打開你的工程,運行單元測試,查看打印區(qū)
啟動插件

- 如果你是新下載代碼,那么可以在 probe-plugin -> Tasks -> intellij -> runIde 中進(jìn)行運行啟動。
單元測試
@Test
public void test_update(){
User user = new User();
user.setId(1L);
user.setName("謝飛機(jī)");
user.setAge(18);
user.setAddress("北京.大興區(qū).亦莊經(jīng)濟(jì)開發(fā)區(qū)");
userDao.update(user);
}
測試結(jié)果
22:30:55.593 [main] DEBUG cn.bugstack.test.demo.infrastructure.dao.UserDao.update[143] - ==> Preparing: UPDATE user SET name=?,age=?,address=? WHERE id=?
22:30:55.625 [main] DEBUG cn.bugstack.test.demo.infrastructure.dao.UserDao.update[143] - ==> Parameters: 謝飛機(jī)(String), 18(Integer), 北京.大興區(qū).亦莊經(jīng)濟(jì)開發(fā)區(qū)(String), 1(Long)
數(shù)據(jù)庫名稱:Mysql
線程ID:1
原始SQL:
UPDATE user SET name=?,age=?,address=?
WHERE id=?
替換SQL:
UPDATE user SET name='謝飛機(jī)',age=18,address='北京.大興區(qū).亦莊經(jīng)濟(jì)開發(fā)區(qū)'
WHERE id=1
- 通過測試結(jié)果可以看到,我們可以獲取到直接拿去測試驗證的 SQL 語句了,就不用在復(fù)制帶問號的 SQL 還得修改后測試了。
五、總結(jié)
- 首先我們是在本章節(jié)初步嘗試使用多模塊的方式來創(chuàng)建工程,這樣的方式可以更加好維護(hù)各類一個工程下所需要的代碼模塊。你也可以嘗試使用 gradle 創(chuàng)建多模塊工程
- 對于字節(jié)碼插樁增強(qiáng)的使用方式,本篇只是一個介紹,這項技術(shù)還可以運用到更多的場景,開發(fā)出各種提升研發(fā)效率的工具。
- 了解額外的 Jar 包是怎么加載到工程的,以及如何通過配置的方式讓 javaagent引入自己開發(fā)好的探針組件。