目錄
一 Agent 模塊
1 HookAgent.java
2 FormatAdvice.java
3 配置文件
二 Attacher 模塊
1 AttachMain.java
三 測試模塊
1 DruidTest.java
四 驗證步驟
五 原理解析
筆者目標寫一款數據分析中間件,用來增強當前主流開源項目,前幾天寫了一票用Byte-Buddy對外部類庫進行增強的方案,但此方案需要在啟動java進程時指定javaagent參數,雖然方案更可靠,但依然覺得對源系統有侵入性。本文針對上一篇文章基礎上實現的完全無侵入增強技術方案。
在 Java 應用開發過程中,我們常常需要對第三方庫或框架的行為進行增強或定制,以滿足特定的業務需求。然而,由于對源代碼不可控或不希望對其進行永久修改,如何在運行時對已有類的方法進行非侵入式動態增強就成為一個重要課題。
本文將詳細介紹如何結合 Java Attach 機制與 Byte Buddy 庫,在不修改原始字節碼文件(非持久化修改)的前提下,對 com.alibaba.druid.sql.SQLUtils.format
方法進行增強,自動為所有符合條件的 SQL 語句追加 LIMIT
限制。整個示例包括三個模塊:
-
agent:字節碼增強代理,實現增強邏輯。
-
attacher:Attach 工具,通過 Attach API 將代理動態注入到目標 JVM。
-
testapp:測試應用,演示增強前后輸出對比。
-
項目結構
父工程采用 Maven 聚合項目(packaging 為
pom
),包含三個子模塊: -
-
agent:構建可重定義/重轉字節碼的 shaded agent jar。
-
attacher:封裝 AttachMain 主程序,用于將 agent 注入到運行中的 JVM。
-
testapp:依賴 Druid,演示 SQL 格式化前后對比。
-
-
父工程 POM
父工程 POM 定義了聚合構建與模塊列表:
<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>org.example</groupId><artifactId>code-gen</artifactId><version>1.0-SNAPSHOT</version><modules><module>agent</module><module>attacher</module><module>testapp</module></modules><packaging>pom</packaging> ?<name>code-gen</name><url>http://maven.apache.org</url> ?<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties> ? </project>
此 POM 僅用于聚合,明確了三個模塊的相對路徑與構建順序。
一 Agent 模塊
Agent 模塊主要依賴 Byte Buddy,并通過 Maven Shade Plugin 打包為一個支持重定義和重轉換的 agent jar。核心配置如下:
<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"><parent><artifactId>code-gen</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion> ?<artifactId>agent</artifactId><packaging>jar</packaging> ?<name>agent</name><url>http://maven.apache.org</url> ?<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties> ?<dependencies><!-- Byte Buddy 核心庫 --><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.14.12</version></dependency> ?<!-- 字節碼增強支持 --><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy-agent</artifactId><version>1.14.12</version></dependency> ?<!-- ASM 字節碼操作 --><dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.6</version></dependency> ?</dependencies> ? ?<build><finalName>agent-shaded</finalName><plugins><!-- 構建 shaded agent jar --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals></execution></executions><configuration><createDependencyReducedPom>false</createDependencyReducedPom><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>agent.HookAgent</mainClass><manifestEntries><Agent-Class>agent.HookAgent</Agent-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></transformer></transformers></configuration></plugin> ?<!-- 編譯插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.10.1</version><configuration><source>8</source><target>8</target></configuration></plugin></plugins></build> </project> ?
1 HookAgent.java
代理入口實現了 agentmain
方法,用于 Attach 注入后的邏輯:
package agent; ? import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; ? import java.lang.instrument.Instrumentation; ? public class HookAgent {public static void agentmain(String args, Instrumentation inst) {try{System.out.println("[Agent] Enhancing SQLUtils.format...");new AgentBuilder.Default().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).type(ElementMatchers.named("com.alibaba.druid.sql.SQLUtils")).transform((builder, td, cl, module, pd) -> builder.method(ElementMatchers.named("format").and(ElementMatchers.takesArguments(2)).and(ElementMatchers.takesArgument(0, String.class))).intercept(Advice.to(FormatAdvice.class))).installOn(inst);}catch (Throwable t){System.err.println("Agent init failed: ");t.printStackTrace();}} }
該邏輯通過 Byte Buddy 定位到 SQLUtils.format(String, DbType)
方法,并將其攔截,委托給 FormatAdvice
。
2 FormatAdvice.java
增強邏輯的具體實現:
package agent; ? import net.bytebuddy.asm.Advice; ? public class FormatAdvice { ?@Advice.OnMethodEnterpublic static void onEnter(@Advice.Argument(value = 0, readOnly = false) String sql) {if (sql == null || sql.toLowerCase().contains("limit")) return; ?sql = sql.trim();if (sql.endsWith(";")) {sql = sql.substring(0, sql.length() - 1);}sql += " LIMIT 100";} }
在方法執行入口,將不含 limit
的 SQL 自動追加 LIMIT 100
。
3 配置文件
resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0 Agent-Class: agent.HookAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
二 Attacher 模塊
Attacher 模塊依賴 JDK 自帶的 tools.jar
,封裝 Attach API 調用:
<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"><parent><artifactId>code-gen</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion> ?<artifactId>attacher</artifactId><packaging>jar</packaging> ?<name>attacher</name><url>http://maven.apache.org</url> ?<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties> ?<dependencies><dependency><groupId>com.sun</groupId><artifactId>tools</artifactId><version>1.8.0</version><scope>system</scope><!-- 替換成自己的 --><systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/lib/tools.jar</systemPath></dependency></dependencies> ? ?<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>attacher.AttachMain</mainClass></transformer></transformers></configuration></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>8</source><target>8</target></configuration></plugin></plugins></build> </project>
1 AttachMain.java
package attacher; ? import com.sun.tools.attach.VirtualMachine; ? public class AttachMain {public static void main(String[] args) throws Exception {if (args.length != 2) {System.out.println("Usage: java AttachMain <agent-path> <PID>");return;}VirtualMachine vm = VirtualMachine.attach(args[1]);vm.loadAgent(args[0]);vm.detach();} }
通過傳入 agent JAR 路徑與目標 JVM PID,即可在運行時注入 HookAgent。無需重啟服務。
三 測試模塊
依賴 Druid,用于演示增強前后 SQL 格式化輸出:
<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"><parent><artifactId>code-gen</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion> ?<artifactId>testapp</artifactId><packaging>jar</packaging> ?<name>testapp</name><url>http://maven.apache.org</url> ?<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties> ?<dependencies><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.18</version></dependency></dependencies> ?<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>app.DruidTest</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins></build> ? </project>
1 DruidTest.java
package app; ? import com.alibaba.druid.DbType; import com.alibaba.druid.sql.SQLUtils; ? /** 測試步驟* 1 啟動服務: java -cp testapp-1.0-SNAPSHOT.jar app.DruidTest* 2 查看服務PID: jps => 16498 DruidTest* 3 注入增強: java --add-modules jdk.attach -cp attacher-1.0-SNAPSHOT.jar attacher.AttachMain agent/target/agent-shaded.jar xxx* 執行結果:* ? ? Before agent:* ? ? SELECT id* ? ? FROM ods__order* ? ? [Agent] Enhancing SQLUtils.format...* ? ? After agent:* ? ? [Agent] Modified SQL: SELECT id FROM ods__order LIMIT 100* ? ? SELECT id* ? ? FROM ods__order* ? ? LIMIT 100*/ public class DruidTest {public static void main(String[] args) throws Exception {String sql = "SELECT id FROM ods__order";System.out.println("Before agent:");System.out.println(SQLUtils.format(sql, DbType.mysql)); ?Thread.sleep(60000); // 等待 Attach 注入 ?System.out.println("After agent:");System.out.println(SQLUtils.format(sql, DbType.mysql));} }
程序啟動后,先打印原始格式化結果,然后等待 60 秒,期間可通過 Attacher 工具注入 agent,最后再次打印格式化結果,以驗證增強效果。
四 驗證步驟
-
構建各模塊
mvn clean package
-
啟動測試應用
java -cp testapp-1.0-SNAPSHOT.jar app.DruidTest
-
查詢應用 PID
jps # 例如輸出: 16498 DruidTest
-
注入增強 Agent
java -cp attacher-1.0-SNAPSHOT.jar attacher.AttachMain agent/target/agent-shaded.jar 16498
-
觀察輸出
-
Before agent:
SELECT id FROM ods__order
-
注入日志:
[Agent] Enhancing SQLUtils.format...
-
After agent:
SELECT id FROM ods__order LIMIT 100
-
五 原理解析
-
Java Attach 機制:
-
Attach API 是 JDK 提供的工具,可動態向運行中的 JVM 進程注入 agent,而無需重啟。
-
通過
VirtualMachine.attach(pid)
方法連接目標 JVM,再調用loadAgent(path)
注入 agent jar。
-
-
Byte Buddy:
-
高級字節碼操作庫,提供易用的 DSL 定義增強規則。
-
支持重定義(redefinition)和重轉換(retransformation)兩種策略。本文使用重轉換,使已有類能夠在運行時更新方法實現。
-
-
Advice 機制:
-
Byte Buddy 的 Advice 注解可在方法入口或退出時插入自定義邏輯,類似 AOP 切面。
-
通過
@Advice.OnMethodEnter
標注的方法可修改參數或執行前置邏輯。
-
本文示例展示了如何結合 Java Attach 與 Byte Buddy,在運行時對第三方庫(Druid)的 SQLUtils.format
方法進行無感知增強,無需修改原始代碼,且對生產環境影響極小。此思路同樣適用于其他需要動態增強的場景,如監控、埋點、執行權限控制等。希望本文能幫助讀者快速上手該技術并在項目中靈活應用。