簡單的bytebuddy學習筆記

在這里插入圖片描述

簡單的bytebuddy學習筆記

此筆記對應b站bytebuddy學習視頻進行整理,此為視頻地址,此處為具體的練習代碼地址

一、簡介

ByteBuddy是基于ASM (ow2.io)實現的字節碼操作類庫。比起ASM,ByteBuddy的API更加簡單易用。開發者無需了解class file format知識,也可通過ByteBuddy完成字節碼編輯。

  • ByteBuddy使用java5實現,并且支持生成JDK6及以上版本的字節碼(由于jdk6和jdk7使用未加密的HTTP類庫, 作者建議至少使用jdk8版本)
  • 和其他字節碼操作類庫一樣,ByteBuddy支持生成類和修改現存類
  • 與與靜態編譯器類似,需要在快速生成代碼和生成快速的代碼之間作出平衡,ByteBuddy主要關注以最少的運行時間生成代碼

Byte Buddy - runtime code generation for the Java virtual machine

JIT優化后的平均ns納秒耗時(標準差)基線Byte BuddycglibJavassistJava proxy
普通類創建0.003 (0.001)142.772 (1.390)515.174 (26.753)193.733 (4.430)70.712 (0.645)
接口實現0.004 (0.001)1’126.364 (10.328)960.527 (11.788)1’070.766 (59.865)1’060.766 (12.231)
stub方法調用0.002 (0.001)0.002 (0.001)0.003 (0.001)0.011 (0.001)0.008 (0.001)
類擴展0.004 (0.001)885.983 5’408.329 (7.901) (52.437)1’632.730 (52.737)683.478 (6.735)
super method invocation0.004 (0.001)0.004 0.004 (0.001) (0.001)0.021 (0.001)0.025 (0.001)

上表通過一些測試,對比各種場景下,不同字節碼生成的耗時。對比其他同類字節碼生成類庫,Byte Buddy在生成字節碼方面整體耗時還是可觀的,并且生成后的字節碼運行時耗時和基線十分相近。

  • Java 代理

    Java 類庫自帶的一個代理工具包,它允許創建實現了一組給定接口的類。這個內置的代理很方便,但是受到的限制非常多。 例如,上面提到的安全框架不能以這種方式實現,因為我們想要擴展類而不是接口。

  • cglib

    代碼生成庫是在 Java 開始的最初幾年實現的,不幸的是,它沒有跟上 Java 平臺的發展。盡管如此,cglib仍然是一個相當強大的庫, 但它是否積極發展變得很模糊。出于這個原因,許多用戶已不再使用它。

    (cglib目前已不再維護,并且github中也推薦開發者轉向使用Byte Buddy)

  • Javassist

    該庫帶有一個編譯器,該編譯器采用包含 Java 源碼的字符串,這些字符串在應用程序運行時被翻譯成 Java 字節碼。 這是非常雄心勃勃的,原則上是一個好主意,因為 Java 源代碼顯然是描述 Java 類的非常的好方法。但是, Javassist 編譯器在功能上無法與 javac 編譯器相比,并且在動態組合字符串以實現更復雜的邏輯時容易出錯。此外, Javassist 帶有一個代理庫,它類似于 Java 的代理程序,但允許擴展類并且不限于接口。然而, Javassist 代理工具的范圍在其API和功能方面同樣受限限制。

    (2023-11-26看javassist在github上一次更新在一年前,而ByteBuddy在3天前還有更新)

二、常用API

我們操作需要先引入對應的pom文件如下:

<dependencyManagement><dependencies><!-- 單元測試 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>RELEASE</version><scope>test</scope></dependency><!-- Byte Buddy --><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.14.10</version></dependency><!-- 工具類 --><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.15.0</version></dependency></dependencies>
</dependencyManagement>

測試模塊對應pom引入包:

<dependencies><!-- 單元測試 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency><!-- Byte Buddy --><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId></dependency><!-- 工具類 --><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId></dependency>
</dependencies>

2.1 生成一個類

2.1.1 注意點

  1. Byte Buddy默認命名策略(NamingStrategy),生成的類名

    1. 父類為jdk自帶類: net.bytebuddy.renamed.{超類名}$ByteBuddy${隨機字符串}
    2. 父類非jdk自帶類 {超類名}$ByteBuddy${隨機字符串}
  2. 如果自定義命名策略,官方建議使用Byte Buddy內置的NamingStrategy.SuffixingRandom

  3. Byte Buddy本身有對生成的字節碼進行校驗的邏輯,可通過.with(TypeValidation.of(false))關閉

  4. .subclass(XXX.class) 指定超類(父類)

  5. .name("packagename.ClassName") 指定類名

    指定name(“cn.git.budy.test.BuddyUserManager”)后生成代碼如下:

    package cn.git.budy.test;import cn.git.UserManager;
    public class BuddyUserManager extends UserManager {public BuddyUserManager() {}
    }
    

2.1.2 示例代碼

package cn.git;import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Test;import java.io.File;
import java.io.IOException;/*** @description: bytebuddy測試類* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public class ByteBuddyTest {/*** 生成文件目錄*/private String path;@Beforepublic void init() {// /D:/idea_workspace/bytebuddy-demo/bytebuddy-demo/bytebuddy-test/target/test-classes/path = ByteBuddyTest.class.getClassLoader().getResource("").getPath();System.out.println(path);}@Testpublic void testCreateClass() throws IOException {// 指定命名策略,生成名稱:UserManager$roadJava$aWAN65zL.class// 非指定生成名稱:UserManager$ByteBuddy$A7LQLGil.classNamingStrategy.SuffixingRandom roadJava = new NamingStrategy.SuffixingRandom("roadJava");// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 不校驗類名稱等校驗.with(TypeValidation.DISABLED)// 指定命名策略.with(roadJava)// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager").make();// 獲取生成類的字節碼byte[] bytes = unloaded.getBytes();// 寫入文件到指定文件FileUtils.writeByteArrayToFile(new File("D:\\SubObj.class"), bytes);// 保存到本地unloaded.saveIn(new File(path));// 將生成的字節碼文件注入到某個jar文件中 C:\Users\Administrator.DESKTOP-40G9I84\Downloads\Desktop (1)\account-server-1.0-SNAPSHOT.jarunloaded.inject(new File("C:\\Users\\Administrator.DESKTOP-40G9I84\\Downloads\\Desktop (1)\\account-server-1.0-SNAPSHOT.jar"));}
}

2.2 對實例方法進行插樁

2.2.1 注意點

程序插樁_百度百科 (baidu.com)

java開發中說的插樁(stub)通常指對字節碼進行修改(增強)。

埋點可通過插樁或其他形式實現,比如常見的代碼邏輯調用次數、耗時監控打點,Android安卓應用用戶操作行為打點上報等。

  • .method(XXX)指定后續需要修改/增強的方法

  • .intercept(XXX)對方法進行修改/增強

    設置攔截toString方法

    指定bytebuddy提供攔截器intercept(FixedValue.value(“hello byteBuddy”))后代碼生成代碼如下:

    package cn.git.budy.test;import cn.git.UserManager;public class BuddyUserManager extends UserManager {public String toString() {return "hello byteBuddy";}public BuddyUserManager() {}
    }
    
  • DynamicType.Unloaded表示未加載到JVM中的字節碼實例

  • DynamicType.Loaded表示已經加載到JVM中的字節碼實例

  • 無特別配置參數的情況下,通過Byte Buddy動態生成的類,實際由net.bytebuddy.dynamic.loading.ByteArrayClassLoader加載

  • 其他注意點,見官方教程文檔的"類加載"章節,這里暫不展開

2.2.2 示例代碼

/*** 對實例方法進行插樁*/
@Test
public void testInstanceMethod() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager")// named通過名字指定要攔截的方法.method(named("toString"))// 指定攔截器,攔截到方法后如何處理.intercept(FixedValue.value("hello byteBuddy")).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends UserManager> loadClass = loaded.getLoaded();// 創建實例調用實例方法UserManager userManager = loadClass.newInstance();String StrResult = userManager.toString();System.out.println(StrResult);// 保存到本地unloaded.saveIn(new File(path));
}

2.3 動態增強的三種方式

2.3.1 注意點

修改/增強現有類主要有3種方法,subclass(創建子類),rebase(變基),redefine(重定義)。

  • .subclass(目標類.class):繼承目標類,以子類的形式重寫超類方法,達到增強效果
  • .rebase(目標類.class):變基,原方法變為private,并且方法名增加&origanl&{隨機字符串}后綴,目標方法體替換為指定邏輯
  • .redefine(目標類.class):重定義,原方法體邏輯直接替換為指定邏輯

根據官方教程文檔,對變基截取如下說明:

class Foo {String bar() { return "bar"; }
}

當對類型變基時,Byte Buddy 會保留所有被變基類的方法實現。Byte Buddy 會用兼容的簽名復制所有方法的實現為一個私有的重命名過的方法, 而不像類重定義時丟棄覆寫的方法。用這種方式的話,不存在方法實現的丟失,而且變基的方法可以通過調用這些重命名的方法, 繼續調用原始的方法。這樣,上面的Foo類可能會變基為這樣

class Foo {String bar() { return "foo" + bar$original(); }private String bar$original() { return "bar"; }
}

其中bar方法原來返回的"bar"保存在另一個方法中,因此仍然可以訪問。當對一個類變基時, Byte Buddy 會處理所有方法,就像你定義了一個子類一樣。例如,如果你嘗試調用變基的方法的超類方法實現, 你將會調用變基的方法。但相反,它最終會扁平化這個假設的超類為上面顯示的變基的類。

2.3.2 示例代碼

修改/增強的目標類SomethingClass

package cn.git;import java.util.UUID;/*** @description:* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public class UserManager {public String selectUserName(Long id) {return "用戶id:" + id + "的名字為:" + UUID.randomUUID().toString();}public void print() {System.out.println(1);}public int selectAge() {return 33;}
}

增強代碼如下:

/*** 動態增強的三種方式* 1.subclass 繼承模式* 2.rebase: 變基,效果是保留原有方法,并且重命名為xxx$original$hash碼信息,xxx則替換為攔截后的邏輯* 3.redefine : 原方法不再保留,xxx為攔截后的邏輯*/
@Test
public void testEnhancement() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager")// named通過名字指定要攔截的方法,還可以使用返回類型進行匹配.method(named("selectUserName").and(returns(TypeDescription.CLASS)).or(returns(TypeDescription.OBJECT)).or(returns(TypeDescription.STRING)))// 指定攔截器,攔截到方法后如何處理.intercept(FixedValue.nullValue()).method(named("print").and(returns(TypeDescription.VOID))).intercept(FixedValue.value(TypeDescription.VOID)).method(named("selectAge")).intercept(FixedValue.value(18)).make();// 保存到本地unloaded.saveIn(new File(path));
}

增強后的代碼如下:

package cn.git.budy.test;import cn.git.UserManager;public class BuddyUserManager extends UserManager {public String toString() {return null;}protected Object clone() throws CloneNotSupportedException {return null;}public void print() {Class var10000 = Void.TYPE;}public String selectUserName(Long var1) {return null;}public int selectAge() {return 18;}public BuddyUserManager() {}
}

我們使用rebase之后,發現生成的代碼沒有xxx$original$hash方法,那是因為我們直接打開是反編譯后的,我們需要使用其他打開方式
在這里插入圖片描述
在這里插入圖片描述

2.4 插入新方法

2.4.1 注意點

  • .defineMethod(方法名, 方法返回值類型, 方法訪問描述符): 定義新增的方法
  • .withParameters(Type...): 定義新增的方法對應的形參類型列表
  • .intercept(XXX): 和修改/增強現有方法一樣,對前面的方法對象的方法體進行修改

具體代碼

/*** 插入新的方法*/
@Test
public void testInsertMethod() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.redefine(UserManager.class).name("cn.git.budy.test.BuddyUserManager")// 定義方法名字以及返回值修飾符.defineMethod("selectUserNameByIds", String.class, Modifier.PUBLIC)// 參數信息.withParameter(String[].class, "ids")// 方法體具體功能.intercept(FixedValue.value("bytebuddy生成的新方法!")).make();// 保存到本地unloaded.saveIn(new File(path));
}

插入新方法后的類如下:

package cn.git.budy.test;import java.util.UUID;public class BuddyUserManager {public BuddyUserManager() {}public String selectUserName(Long id) {return "用戶id:" + id + "的名字為:" + UUID.randomUUID().toString();}public void print() {System.out.println(1);}public int selectAge() {return 33;}public String selectUserNameByIds(String[] ids) {return "bytebuddy生成的新方法!";}
}

2.5 插入新屬性

2.5.1 注意點

  • .defineField(String name, Type type, int modifier): 定義成員變量
  • .implement(Type interfaceType): 指定實現的接口類
  • .intercept(FieldAccessor.ofField("成員變量名").intercept(FieldAccessor.ofBeanProperty())在實現的接口為Bean規范接口時,都能生成成員變量對應的getter和setter方法

視頻使用intercept(FieldAccessor.ofField("成員變量名"),而官方教程的"訪問字段"章節使用.intercept(FieldAccessor.ofBeanProperty())來生成getter和setter方法

2.5.2 示例代碼

后續生成getter, setter方法需要依賴的接口類定義

package cn.git;/*** @description:* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public interface UserAgentInterface {void setAge(int age);int getAge();
}

插入新屬性基礎代碼:

/*** 新增屬性* 使用.intercept(FieldAccessor.ofField("age"))和使用.intercept(FieldAccessor.ofBeanProperty())在這里效果是一樣的*/
@Test
public void testAddField() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.redefine(UserManager.class).name("cn.git.budy.test.BuddyUserManager")// 定義方法名字以及返回值修飾符.defineField("age",  int.class, Modifier.PRIVATE)// 指定age對應get以及set方法所在的接口,進行實現.implement(UserAgentInterface.class)//指定getter和setter方法.intercept(FieldAccessor.ofField("age")).make();// 保存到本地unloaded.saveIn(new File(path));
}

2.6 方法委托

2.6.1 注意點

方法委托,可簡單理解將目標方法的方法體邏輯修改為調用指定的某個輔助類方法。

  • .intercept(MethodDelegation.to(Class<?> type)):將被攔截的方法委托給指定的增強類,增強類中需要定義和目標方法一致的方法簽名,然后多一個static訪問標識
  • .intercept(MethodDelegation.to(Object target)):將被攔截的方法委托給指定的增強類實例,增強類可以指定和目標類一致的方法簽名,或通過@RuntimeType指示 Byte Buddy 終止嚴格類型檢查以支持運行時類型轉換。

其中委托給相同簽名的靜態方法/實例方法相對容易理解,委托給自定義方法時,該視頻主要介紹幾個使用到的方法參數注解:

  • @This Object targetObj:表示被攔截的目標對象, 只有攔截實例方法時可用
  • @Origin Method targetMethod:表示被攔截的目標方法, 只有攔截實例方法或靜態方法時可用
  • @AllArguments Object[] targetMethodArgs:目標方法的參數
  • @Super Object targetSuperObj:表示被攔截的目標對象, 只有攔截實例方法時可用 (可用來調用目標類的super方法)。若明確知道具體的超類(父類類型),這里Object可以替代為具體超類(父類)
  • @SuperCall Callable<?> zuper:用于調用目標方法

其中調用目標方法時,通過Object result = zuper.call()。不能直接通過反射的Object result = targetMethod.invoke(targetObj,targetMethodArgs)進行原方法調用。因為后者會導致無限遞歸進入當前增強方法邏輯。

方法委托部分我們要使用一些新的注解,在interceptor進行使用,具體注解如下:

注解說明
@Argument綁定單個參數
@AllArguments綁定所有參數的數組
@This當前被攔截的、動態生成的那個對象
@Super當前被攔截的、動態生成的那個對象,不會繼承原有的類
@Origin可以綁定到以下類型的參數: - Method 被調用的原始方法 - Constructor 被調用的原始構造器 - Class 當前動態創建的類 - MethodHandleMethodTypeString 動態類的toString()的返回值 - int 動態方法的修飾符
@DefaultCall調用默認方法而非super的方法
@SuperCall用于調用父類版本的方法
@RuntimeType可以用在返回值、參數上,提示ByteBuddy禁用嚴格的類型檢查
@Empty注入參數的類型的默認值
@StubValue注入一個存根值。對于返回引用、void的方法,注入null;對于返回原始類型的方法,注入0
@FieldValue注入被攔截對象的一個字段的值
@Morph類似于@SuperCall,但是允許指定調用參數

其他具體細節和相關介紹,可參考[官方教程](Byte Buddy - runtime code generation for the Java virtual machine)的"委托方法調用"章節。尤其是各種注解的介紹,官方教程更加完善一些,但是相對比較晦澀難懂一點。

2.6.2 示例代碼

2.6.2.1 委托方法給相同方法簽名方法

接收委托的類,定義和需要修改/增強的目標類中的指定方法的方法簽名(方法描述符)一致的方法,僅多static訪問修飾符

package cn.git;import java.util.UUID;public class UserManagerInterceptor {public static String selectUserName(Long id) {return "UserManagerInterceptor 用戶id:" + id + "的名字為:" + UUID.randomUUID().toString();}
}

主方法代碼為:

/*** 方法委托,使用自己自定義的攔截器*/
@Test
public void testMethodDelegation() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager").method(named("selectUserName"))// 委托給UserManagerInterceptor中的同名selectUserName的靜態方法// 如果不想使用靜態方法,可以指定為實例方法,即.to(new UserManagerInterceptor()).intercept(MethodDelegation.to(UserManagerInterceptor.class)).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends UserManager> loadClass = loaded.getLoaded();// 創建實例調用實例方法UserManager userManager = loadClass.newInstance();String StrResult = userManager.selectUserName(1521L);System.out.println(StrResult);// 保存到本地unloaded.saveIn(new File(path));}

非靜態方法則是調用時候使用 .intercept(MethodDelegation.to(UserManagerInterceptor.class))即可

委托后的代碼如下:

package cn.git.budy.test;import cn.git.UserManager;
import cn.git.UserManagerInterceptor;public class BuddyUserManager extends UserManager {public String selectUserName(Long var1) {return UserManagerInterceptor.selectUserName(var1);}public BuddyUserManager() {}
}
2.6.2.2 方法委托非同簽名的方法

攔截方法的具體實現

package cn.git;import net.bytebuddy.implementation.bind.annotation.*;import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;public class UserManagerDiffMethodNameInterceptor {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic Object diffNameMethod(// 被攔截的目標對象,表示只有攔截實例方法或者構造方法時可用@This Object targetObject,// 被攔截的目標方法,攔截實例方法以及靜態方法有效@Origin Method targetMethod,// 被攔截的目標方法參數,攔截實例方法以及靜態方法有效@AllArguments Object[] targetMethodArgs,// 被攔截的目標方法父類,攔截實例方法或者構造方法有效// 若確定父類,則可以使用 @Super UserManager superObject@Super Object superObject,// 用于調用目標方法@SuperCall Callable<?> superCall) {// cn.git.budy.test.BuddyUserManager@a1f72f5System.out.println("targetObject : " + targetObject);// selectUserNameSystem.out.println("targetMethodName : " + targetMethod.getName());// [1521]System.out.println("targetMethodArgs : " + Arrays.toString(targetMethodArgs));// cn.git.budy.test.BuddyUserManager@a1f72f5System.out.println("superObject : " + superObject);Object call;try {// 調用目標方法,打印 用戶id:1521的名字為:030a0667-b02b-4795-bcc7-3b99c84f18c4// 不可以使用 targetMethod.invoke 會引起遞歸調用call = superCall.call();} catch (Exception e) {throw new RuntimeException(e);}return call;}
}

主方法代碼為:

/*** 方法委托,使用自己自定義的攔截器*/@Testpublic void testMethodDelegation() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager").method(named("selectUserName"))// 委托給UserManagerInterceptor中的同名selectUserName的靜態方法// 如果不想使用靜態方法,可以指定為實例方法,即.to(new UserManagerInterceptor())// .intercept(MethodDelegation.to(UserManagerInterceptor.class))// 不同簽名的方法.intercept(MethodDelegation.to(new UserManagerDiffMethodNameInterceptor())).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends UserManager> loadClass = loaded.getLoaded();// 創建實例調用實例方法UserManager userManager = loadClass.newInstance();String StrResult = userManager.selectUserName(1521L);System.out.println(StrResult);// 保存到本地unloaded.saveIn(new File(path));}

編譯后會生成多個類如下所示:
在這里插入圖片描述

2.7 動態修改入參

2.7.1 注意點

  • @Morph:和@SuperCall功能基本一致,主要區別在于@Morph支持傳入參數

  • 使用@Morph時,需要在攔截方法注冊代理類/實例前,指定install注冊配合@Morph使用的函數式接口,其入參必須為Object[]類型,并且返回值必須為Object類型

    .intercept(MethodDelegation.withDefaultConfiguration()// 向Byte Buddy 注冊 用于中轉目標方法入參和返回值的 函數式接口.withBinders(Morph.Binder.install(MyCallable.class)).to(new SomethingInterceptor04()))
    

    java源代碼中@Mopth的文檔注釋如下:

    /*** This annotation instructs Byte Buddy to inject a proxy class that calls a method's super method with* explicit arguments. For this, the {@link Morph.Binder}* needs to be installed for an interface type that takes an argument of the array type {@link java.lang.Object} and* returns a non-array type of {@link java.lang.Object}. This is an alternative to using the* {@link net.bytebuddy.implementation.bind.annotation.SuperCall} or* {@link net.bytebuddy.implementation.bind.annotation.DefaultCall} annotations which call a super* method using the same arguments as the intercepted method was invoked with.** @see net.bytebuddy.implementation.MethodDelegation* @see net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder*/
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface Morph {...
    }
    

2.7.2 示例代碼

新增MyCallable代碼

package cn.git;/*** @description: 用于后續接收目標方法的參數, 以及中轉返回值的函數式接口,入參必須是 Object[], 返回值必須是 Object* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public interface MyCallable {Object call(Object[] args);
}

執行邏輯攔截器方法:

package cn.git;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Morph;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;public class UserManagerDynamicParamInterceptor {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic Object diffNameMethod(// 被攔截的目標方法參數,攔截實例方法以及靜態方法有效@AllArguments Object[] targetMethodArgs,// 用于調用目標方法@Morph MyCallable myCallable) {Object call;try {// 不可以使用 targetMethod.invoke 會引起遞歸調用if (targetMethodArgs != null && targetMethodArgs.length > 0) {targetMethodArgs[0] = Long.valueOf(targetMethodArgs[0].toString()) + 1;}call = myCallable.call(targetMethodArgs);} catch (Exception e) {throw new RuntimeException(e);}return call;}}

主方法如下:

/*** 動態修改入參* 1.自定義一個Callable接口類* 2.在攔截器接口中使用@Morph注解,代替之前的@SuperCall注解* 3.指定攔截器之前調用withBinders*/
@Test
public void testMethodArgumentModifier() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager").method(named("selectUserName")).intercept(MethodDelegation.withDefaultConfiguration()// 在UserManagerDynamicParamInterceptor中使用MyCallable之前,告訴bytebuddy參數類型是myCallable.withBinders(Morph.Binder.install(MyCallable.class)).to(new UserManagerDynamicParamInterceptor())).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends UserManager> loadClass = loaded.getLoaded();// 創建實例調用實例方法,預期結果 101UserManager userManager = loadClass.newInstance();String StrResult = userManager.selectUserName(100L);System.out.println(StrResult);// 保存到本地unloaded.saveIn(new File(path));
}

運行結果如下:

在這里插入圖片描述

2.8 對構造方法進行插樁

2.8.1 注意點

  • .constructor(ElementMatchers.any()): 表示攔截目標類的任意構造方法
  • .intercept(SuperMethodCall.INSTANCE.andThen(Composable implementation): 表示在實例構造方法邏輯執行結束后再執行攔截器中定義的增強邏輯
  • @This: 被攔截的目標對象this引用,構造方法也是實例方法,同樣有this引用可以使用

2.8.2 示例代碼

給需要增強的類上新增構造方法,方便后續掩飾構造方法插樁效果

package cn.git;import java.util.UUID;/*** @description:* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public class UserManager {/*** 新增構造方法*/public UserManager() {System.out.println("UserManager 構造函數");}public String selectUserName(Long id) {return "用戶id:" + id + "的名字為:" + UUID.randomUUID().toString();}public void print() {System.out.println(1);}public int selectAge() {return 33;}
}

新建用于增強構造器方法的攔截器類,里面描述構造方法直接結束后,后續執行的邏輯

package cn.git;import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;/*** @description: 構造方法攔截器* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-18*/
public class UserManagerConstructorInterceptor {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic void diffNameMethod(@This Object targetObject) {System.out.println(targetObject + " 實例化了");}
}

主方法:

/*** 構造方法插樁*/
@Test
public void testConstructorInterceptor() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.subclass(UserManager.class).name("cn.git.budy.test.BuddyUserManager")// 攔截構造方法.constructor(any()).intercept(// 指定在構造方法執行完畢后再委托給攔截器SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(new UserManagerConstructorInterceptor()))).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends UserManager> loadClass = loaded.getLoaded();// 創建實例調用實例方法,預期結果 101UserManager userManager = loadClass.newInstance();String StrResult = userManager.selectUserName(100L);System.out.println(StrResult);// 保存到本地unloaded.saveIn(new File(path));
}

2.9 對靜態方法進行插樁

2.9.1 注意點

  • 增強靜態方法時,通過@This@Super獲取不到目標對象
  • 增強靜態方法時,通過@Origin Class<?> clazz可獲取靜態方法所處的Class對象

2.9.2 示例代碼

我們使用FileUtil.sizeOf方法作為插樁方法,編輯靜態方法增強類

package cn.git;import net.bytebuddy.implementation.bind.annotation.*;import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;public class UserManagerStatic {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic Object diffNameMethod(// 被攔截的目標對象,靜態方法只能拿取到class類對象,拿取不到This對象@Origin Class<?> targetClass,// 被攔截的目標方法,攔截實例方法以及靜態方法有效@Origin Method targetMethod,// 被攔截的目標方法參數,攔截實例方法以及靜態方法有效@AllArguments Object[] targetMethodArgs,// 用于調用目標方法@SuperCall Callable<?> superCall) {// cn.git.budy.test.BuddyUserManager@a1f72f5System.out.println("targetClass : " + targetClass);// selectUserNameSystem.out.println("targetMethodName : " + targetMethod.getName());// [1521]System.out.println("targetMethodArgs : " + Arrays.toString(targetMethodArgs));Object call;try {// 調用目標方法,打印 用戶id:1521的名字為:030a0667-b02b-4795-bcc7-3b99c84f18c4// 不可以使用 targetMethod.invoke 會引起遞歸調用call = superCall.call();} catch (Exception e) {throw new RuntimeException(e);}return call;}}

編輯主類:

/*** 靜態方法插樁*/
@Test
public void testStaticMethodInterceptor() throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {long size = FileUtils.sizeOf(new File("D:\\SubObj.class"));System.out.println(size);// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<FileUtils> unloaded = new ByteBuddy()// 變基.rebase(FileUtils.class).name("cn.git.budy.test.BuddyUserManager")// 通過名稱sizeOf攔截靜態方法.method(named("sizeOf").and(isStatic())).intercept(MethodDelegation.to(new UserManagerStatic())).make();// loaded表示字節碼已經加載到jvm中// loaded同樣擁有saveIn,getBytes,inject方法,unloaded繼承自DynamicTypeDynamicType.Loaded<FileUtils> loaded = unloaded.load(getClass().getClassLoader());// 獲取加載的類Class<? extends FileUtils> loadClass = loaded.getLoaded();Method sizeOfMethod = loadClass.getMethod("sizeOf", File.class);Object fileSize = sizeOfMethod.invoke(null, new File("D:\\SubObj.class"));System.out.println(fileSize.toString());unloaded.saveIn(new File(path));
}

調用結果展示如下:

在這里插入圖片描述

2.10 @SuperCall, rebase, redefine, subclass

2.10.1 注意點

  • @SuperCall僅在原方法仍存在的場合能夠正常使用,比如subclass超類方法仍為目標方法,而rebase則是會重命名目標方法并保留原方法體邏輯;但redefine直接替換掉目標方法,所以@SuperCall不可用
  • rebaseredefine都可以修改目標類靜態方法,但是若想在原靜態方法邏輯基礎上增加其他增強邏輯,那么只有rebase能通過@SuperCall@Morph調用到原方法邏輯;redefine不保留原目標方法邏輯

2.10.2 示例代碼

這里使用的示例代碼和"2.9.2 示例代碼"一致,主要是用于說明前面"2.9 對靜態方法進行插樁"時為什么只能用rebase,而不能用subclass;以及使用rebase后,整個增強的大致調用流程。

  • subclass:以目標類子類的形式,重寫父類方法完成修改/增強。子類不能重寫靜態方法,所以增強目標類的靜態方法時,不能用subclass
  • redefine:因為redefine不保留目標類原方法,所以UserManagerStatic中的diffNameMethod方法獲取不到@SuperCall Callable<?> superCall參數,若注解掉superCall相關的代碼,發現能正常運行,但是目標方法相當于直接被替換成我們的邏輯,達不到保留原方法邏輯并增強的目的。
  • rebase:原方法會被重命名并保留原邏輯,所以能夠在通過@SuperCall Callable<?> superCall保留執行原方法邏輯執行的情況下,繼續執行我們自定義的修改/增強邏輯

使用rebase生成了兩個class,一個為BuddyUserManager.class,一個為輔助類BuddyUserManager$auxiliary$5FSta4Vk

public static long sizeOf(File var0) {return (Long)delegate$rrhahm1.diffNameMethod(BuddyUserManager.class, cachedValue$EZYLMYyp$hh4d832, new Object[]{var0}, new BuddyUserManager$auxiliary$5FSta4Vk(var0));
}

2.11 rebase, redefine默認生成類名

subclass, rebase, redefine各自的默認命名策略如下:

  • .subclass(目標類.class)
    • 超類為jdk自帶類: net.bytebuddy.renamed.{超類名}$ByteBuddy${隨機字符串}
    • 超類非jdk自帶類 {超類名}$ByteBuddy${隨機字符串}
  • .rebase(目標類.class):和目標類的類名一致(效果上即覆蓋原本的目標類class文件)
  • .redefine(目標類.class):和目標類的類名一致(效果上即覆蓋原本的目標類class文件)

這里就不寫示例代碼了,實驗的方式很簡單,即把自己指定的類名.name(yyy.zzz.Xxxx)去掉,即根據默認命名策略生成類名

2.12 bytebuddy的類加載器

2.12.1 注意點

  • DynamicType.Unloaded<SomethingClass>實例.load(getClass().getClassLoader()).getLoaded()等同于DynamicType.Unloaded<SomethingClass>實例.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded()

    Byte Buddy默認使用WRAPPER類加載策略,該策略會優先根據類加載的雙親委派機制委派父類加載器加載指定類,若類成功被父類加載器加載,此處仍通過.load加載類就報錯。(直觀上就是將生成的類的.class文件保存到本地后,繼續執行.load方法會拋異常java.lang.IllegalStateException: Class already loaded

  • 若使用CHILD_FIRST類加載策略,那么打破雙親委派機制,優先在當前類加載器加載類(直觀上就是將生成的類的.class文件保存到本地后,繼續執行.load方法不會報錯,.class類由ByteBuddy的ByteArrayClassLoader正常加載)。具體代碼可見net.bytebuddy.dynamic.loading.ByteArrayClassLoader.ChildFirst#loadClass

下面摘出net.bytebuddy.dynamic.loading.ByteArrayClassLoader.ChildFirst#loadClass源代碼

/*** Loads the class with the specified <a href="#binary-name">binary name</a>.  The* default implementation of this method searches for classes in the* following order:** @param   name*          The <a href="#binary-name">binary name</a> of the class** @param   resolve*          If {@code true} then resolve the class** @return  The resulting {@code Class} object** @throws  ClassNotFoundException*          If the class could not be found*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (SYNCHRONIZATION_STRATEGY.initialize().getClassLoadingLock(this, name)) {Class<?> type = findLoadedClass(name);if (type != null) {return type;}try {type = findClass(name);if (resolve) {resolveClass(type);}return type;} catch (ClassNotFoundException exception) {// If an unknown class is loaded, this implementation causes the findClass method of this instance// to be triggered twice. This is however of minor importance because this would result in a// ClassNotFoundException what does not alter the outcome.return super.loadClass(name, resolve);}}
}

其他關于類加載的介紹,可以查閱Byte Buddy官方教程文檔的"類加載"章節,下面內容摘自官方教程文檔

目前為止,我們只是創建了一個動態類型,但是我們并沒有使用它。Byte Buddy 創建的類型是通過DynamicType.Unloaded的一個實例來表示的。通過名稱可以猜到,這些類不會加載到JVM。 相反,Byte Buddy 創建的類以Java 類文件格式的二進制結構表示。 這樣的話,你可以決定用生成的類來做什么。例如,你或許想從構建腳本運行 Byte Buddy,該腳本僅在部署前生成類以增強 Java 應用。 對于這個目的,DynamicType.Unloaded類允許提取動態類型的字節數組。為了方便, 該類型還額外提供了saveIn(File)方法,該方法允許你將一個類保存到給定的文件夾。此外, 它允許你通過inject(File)方法將類注入到已存在的 jar 文件。

雖然直接訪問一個類的二進制結構是直截了當的,但不幸的是加載一個類更復雜。在 Java 里,所有的類都用ClassLoader(類加載器)加載。 這種類加載器的一個示例是啟動類加載器,它負責加載 Java 類庫里的類。另一方面,系統類加載器負責加載 Java 應用程序類路徑里的類。 顯然,這些預先存在的類加載器都不知道我們創建的任何動態類。為了解決這個問題,我們需要找其他的可能性用于加載運行時生成的類。 Byte Buddy 通過開箱即用的不同方法提供解決方案:

  • 我們僅僅創建一個新的ClassLoader,它被明確地告知存在一個特定的動態創建的類。 因為 Java 類加載器是按層級組織的,我們定義的這個類加載器是程序里已經存在的類加載器的孩子。這樣, 程序里的所有類對于新類加載器加載的動態類型都是可見的。
  • 通常,Java 類加載器在嘗試直接加載給定名稱的類之前會詢問他的父類加載器。這意味著,在父類加載器知道有相同名稱的類時, 子類加載器通常不會加載類。為此,Byte Buddy 提供了孩子優先創建的類加載器,它在詢問父類加載器之前會嘗試自己加載類。 除此之外,這種方法類似于剛才上面提及的方法。注意,這種方法不會覆蓋父類加載器加載的類,而是隱藏其他類型。
  • 最后,我們可以用反射將一個類注入到已存在的類加載器。通常,類加載器會被要求通過類名稱來提供一個給定的類。 用反射我們可以扭轉這個規則,調用受保護的方法將一個新類注入類加載器,而類加載器實際上不知道如何定位這個動態類。

不幸的是,上面的方法都有其缺點:

  • 如果我們創建一個新的ClassLoader,這個類加載器會定義一個新的命名空間。 這樣可能會通過兩個不同的類加載器加載兩個有相同名稱的類。這兩個類永遠不會被JVM視為相等,即時這兩個類是相同的類實現。 這個相等規則也適用于 Java 包。這意味著,如果不是用相同的類加載器加載, example.Foo類無法訪問example.Bar類的包私有方法。此外, 如果example.Bar繼承example.Foo,任何被覆寫的包私有方法都將變為無效,但會委托給原始實現。
  • 每當加載一個類時,一旦引用另一種類型的代碼段被解析,它的類加載器將查找該類中引用的所有類型。該查找會委托給同一個類加載器。 想象一下這種場景:我們動態的創建了example.Fooexample.Bar兩個類, 如果我們將example.Foo注入一個已經存在的類加載器,這個類加載器可能會嘗試定位查找example.Bar。 然而,這個查找會失敗,因為后一個類是動態創建的,而且對于剛才注入example.Foo類的類加載器來說是不可達的。 因此反射的方法不能用于在類加載期間生效的帶有循環依賴的類。幸運的是,大多數JVM的實現在第一次使用時都會延遲解析引用類, 這就是類注入通常在沒有這些限制的時候正常工作的原因。此外,實際上,由 Byte Buddy 創建的類通常不會受這樣的循環影響

你可能會任務遇到循環依賴的機會是無關緊要的,因為一次只創建一個動態類。然而,動態類型的創建可能會觸發輔助類型的創建。 這些類型由 Byte Buddy 自動創建,以提供對正在創建的動態類型的訪問。我們將在下面的章節學習輔助類型,現在不要擔心這些。 但是,正因為如此,我們推薦你盡可能通過創建一個特定的ClassLoader來加載動態類, 而不是將他們注入到一個已存在的類加載器。

創建一個DynamicType.Unloaded后,這個類型可以用ClassLoadingStrategy加載。 如果沒有提供這個策略,Byte Buddy 會基于提供的類加載器推測出一種策略,并且僅為啟動類加載器創建一個新的類加載器, 該類加載器不能用反射的方式注入任何類。否則為默認設置。

Byte Buddy 提供了幾種開箱即用的類加載策略, 每一種都遵循上述概念中的其中一個。這些策略都在ClassLoadingStrategy.Default中定義,其中, WRAPPER策略會創建一個新的,經過包裝的ClassLoaderCHILD_FIRST策略會創建一個類似的具有孩子優先語義的類加載器,INJECTION策略會用反射注入一個動態類型

WRAPPERCHILD_FIRST策略也可以在所謂的*manifest(清單)*版本中使用,即使在類加載后, 也會保留類的二進制格式。這些可替代的版本使類加載器加載的類的二進制表示可以通過ClassLoader::getResourceAsStream方法訪問。 但是,請注意,這需要這些類加載器保留一個類的完整的二進制表示的引用,這會占用 JVM 堆上的空間。因此, 如果你打算實際訪問類的二進制格式,你應該只使用清單版本。由于INJECTION策略通過反射實現, 而且不可能改變方法ClassLoader::getResourceAsStream的語義,因此它自然在清單版本中不可用。

讓我們看一下這樣的類加載:

Class<?> type = new ByteBuddy().subclass(Object.class).make().load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded();

在上面的示例中,我們創建并加載了一個類。像我們之前提到的,我們用WRAPPER加載策略加載類, 它適用于大多數場景。最后,getLoaded方法返回了一個現在已經加載的 Java Class(類)的實例, 這個實例代表著動態類。

注意,當加載類時,預定義的類加載策略是通過應用執行上下文的ProtectionDomain來執行的。或者, 所有默認的策略通過調用withProtectionDomain方法來提供明確地保護域規范。 當使用安全管理器或使用簽名jar包中定義的類時,定義一個明確地保護域是非常重要的。

2.13 自定義類的加載路徑

2.13.1 注意點

  • ClassFileLocator:類定位器,用來定位類文件所在的路徑,支持jar包所在路徑,.class文件所在路徑,類加載器等。
    • ClassFileLocator.ForJarFile.of(File file):jar包所在路徑
    • ClassFileLocator.ForFolder(File file).class文件所在路徑
    • ClassFileLocator.ForClassLoader.ofXxxLoader():類加載器
    • 一般使用時都需要帶上ClassFileLocator.ForClassLoader.ofSystemLoader(),才能保證jdk自帶類能夠正常被掃描識別到,否則會拋出異常(net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description for java.lang.Object)。
  • ClassFileLocator.Compound:本身也是類定位器,用于整合多個ClassFileLocator
  • TypePool:類型池,一般配合ClassFileLocator.Compound使用,用于從指定的多個類定位器內獲取類描述對象
    • 調用typePool.describe("全限制類名").resolve()獲取TypeDescription類描述對象,resolve()不會觸發類加載。
  • TypeDescription:類描述對象,用于描述java類,后續subclass, rebase, redefine時用于指定需要修改/增改的類。

其他介紹可見官方教程文檔的"重新加載類"和"使用未加載的類"章節,下面內容摘至官方教程文檔:

使用 Java 的 HotSwap 功能有一個巨大的缺陷,HotSwap的當前實現要求重定義的類在重定義前后應用相同的類模式。 這意味著當重新加載類時,不允許添加方法或字段。我們已經討論過 Byte Buddy 為任何變基的類定義了原始方法的副本, 因此類的變基不適用于ClassReloadingStrategy。此外,類重定義不適用于具有顯式的類初始化程序的方法(類中的靜態塊)的類, 因為該初始化程序也需要復制到額外的方法中。不幸的是, OpenJDK已經退出了擴展HotSwap的功能, 因此,無法使用HotSwap的功能解決此限制。同時,Byte Buddy 的HotSwap支持可用于某些看起來有用的極端情況。 否則,當(例如,從構建腳本)增強存在的類時,變基和重定義可能是一個便利的功能。

意識到HotSwap功能的局限性后,人們可能會認為變基重定義指令的唯一有意義的應用是在構建期間。 通過應用構建時的處理,人們可以斷言一個已經處理過的類在它的初始類簡單地加載之前沒有被加載,因為這個類加載是在不同的JVM實例中完成的。 然而,Byte Buddy 同樣有能力處理尚未加載的類。為此,Byte Buddy 抽象了 Java 的反射 API,例如, 一個Class實例在內部由一個TypeDescription表示。事實上, Byte Buddy 只知道如何處理由實現了TypeDescription接口的適配器提供的Class。 這種抽象的最大好處是類的信息不需要由類加載器提供,而是可以由其他的源提供。

**Byte Buddy 使用TypePool(類型池),提供了一種標準的方式來獲取類的TypeDescription(類描述)**。當然, 這個池的默認實現也提供了。TypePool.Default的實現解析類的二進制格式并將其表示為需要的TypeDescription。 類似于類加載器為加載好的類維護一個緩存,該緩存也是可定制的。此外,它通常從類加載器中檢索類的二進制格式, 但不指示它加載此類

示例代碼:

我要插樁某一個其他路徑下的包類信息,spring-beans-5.2.12.RELEASE.jar 里面的 RootBeanDefinition類中的 getTargetType方法,返回一個空值

/*** 自定義類的加載路徑*/
@Test
public void testCustomClassLoader() throws IOException, InstantiationException, IllegalAccessException {// 從指定jar包加載,可能是外部包ClassFileLocator beansJarFileLocator = ClassFileLocator.ForJarFile.of(new File("D:\\apache-maven-3.6.3\\repos\\org\\springframework\\spring-beans\\5.2.12.RELEASE\\spring-beans-5.2.12.RELEASE.jar"));ClassFileLocator coreJarFileLocator = ClassFileLocator.ForJarFile.of(new File("D:\\apache-maven-3.6.3\\repos\\org\\springframework\\spring-core\\5.2.12.RELEASE\\spring-core-5.2.12.RELEASE.jar"));// 從指定目錄加載 .class 文件ClassFileLocator.ForFolder jarFolder = new ClassFileLocator.ForFolder(new File("D:\\idea_workspace\\bank-credit-sy\\credit-support\\credit-uaa\\uaa-server\\target\\classes"));// 系統類加載器,如果不加會找不到jdk本身的類ClassFileLocator systemLoader = ClassFileLocator.ForClassLoader.ofSystemLoader();// 創建一個組合類加載器ClassFileLocator.Compound compound = new ClassFileLocator.Compound(beansJarFileLocator, systemLoader, coreJarFileLocator, jarFolder);TypePool typePool = TypePool.Default.of(compound);// 寫入全類名稱,獲取對應對象,并不會觸發類的加載TypeDescription typeDescription = typePool.describe("org.springframework.beans.factory.support.RootBeanDefinition").resolve();// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<Object> unloaded = new ByteBuddy()// 變基.redefine(typeDescription, compound).name("cn.git.budy.test.BuddyUserManager")// 通過名稱sizeOf攔截靜態方法.method(named("getTargetType")).intercept(FixedValue.nullValue()).make();unloaded.saveIn(new File(path));// 加載文件夾中的類TypeDescription typeDescriptionClassFolder = typePool.describe("cn.git.auth.dto.HomeDTO").resolve();DynamicType.Unloaded<Object> classFolderUnLoaded = new ByteBuddy()// 變基.redefine(typeDescriptionClassFolder, compound).name("cn.git.budy.test.BuddyUserManagerClassFolder")// 通過名稱sizeOf攔截靜態方法.method(named("getCurrentLoginUserCd")).intercept(FixedValue.nullValue()).make();classFolderUnLoaded.saveIn(new File(path));
}

最終生成代碼效果如下:

在這里插入圖片描述
在這里插入圖片描述

2.14 清空方法體

2.14.1 注意點

  • ElementMatchers.isDeclaredBy(Class<?> type)):攔截僅由目標類聲明的方法,通常用于排除超類方法
  • StubMethod.INSTANCE:Byte Buddy默認的攔截器方法實現之一,會根據被攔截的目標方法的返回值類型返回對應的默認值
    1. The value 0 for all numeric type.
    2. The null character for the char type.
    3. false for the boolean type.
    4. Nothing for void types.
    5. A null reference for any reference types. Note that this includes primitive wrapper types.
  • 當使用ElementMatchers.any()時,僅subclass包含構造方法,rebaseredefine不包含構造方法
  • 使用ElementMatchers.any().and(ElementMatchers.isDeclaredBy(目標類.class))時,僅subclass支持修改生成類名,rebaseredefine若修改類名則攔截后的修改/增強邏輯無效。

演示代碼:

/*** 清空方法體,起到保護源碼的作用*/
@Test
public void testEmptyMethodBody() throws IOException, InstantiationException, IllegalAccessException {// unloaded表示字節碼還未加載到jvm中DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()// 指定父類.redefine(UserManager.class)// .name("cn.git.budy.test.BuddyUserManager")// named通過名字指定要攔截的方法,還可以使用返回類型進行匹配// .and(isDeclaredBy(UserManager.class)) 父類方法重寫清空 equals,toString,hashCode.method(any())// 預制攔截器清空方法.intercept(StubMethod.INSTANCE).make();// 保存到本地unloaded.saveIn(new File(path));
}

三、java agent

3.1 原生jdk實現

3.1.1 注意點

  • premain方法在main之前執行
  • Instrumentation#addTransformer(ClassFileTransformer transformer):注冊字節碼轉換器,這里在premain方法內注冊,保證在main方法執行前就完成字節碼轉換
  • 字節碼中類名以/間隔,而不是.間隔

關于java agent,網上也有很多相關文章,這里不多做介紹,這里簡單鏈接一些文章:

一文講透Java Agent是什么玩意?能干啥?怎么用? - 知乎 (zhihu.com)

Java探針(javaagent) - 簡書 (jianshu.com)

初探Java安全之JavaAgent - SecIN社區 - 博客園 (cnblogs.com)

java.lang.instrument (Java SE 21 & JDK 21) (oracle.com)

3.1.2 示例代碼

新建一個module為agent-jdk,這里圖方便,里面主要實現了premain方法,以及一個簡單的例子,對一個自定義類TestService類的加強,引入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><parent><groupId>cn.git</groupId><artifactId>bytebuddy-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>agent-jdk</artifactId><packaging>jar</packaging><name>agent-jdk</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.28.0-GA</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.6</version></dependency></dependencies><build><plugins><plugin><!-- 用于打包插件 --><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.1.0</version><configuration><archive><manifestEntries><!-- MANIFEST.MF 配置項,指定premain方法所在類 --><Premain-Class>cn.git.AgentDemo</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes><Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix></manifestEntries></archive><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs></configuration><executions><execution><id>make-assembly</id><!-- 什么階段會觸發此插件 --><phase>package</phase><goals><!-- 只運行一次 --><goal>single</goal></goals></execution></executions></plugin></plugins></build>
</project>

探針的premain接口實現:

package cn.git;import lombok.extern.slf4j.Slf4j;import java.lang.instrument.Instrumentation;/*** @description: 探針啟動入口* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class AgentDemo {/*** premain方法,main方法執行之前進行調用,插樁代碼入口* @param args 標識外部傳遞參數* @param instrumentation 插樁對象*/public static void premain(String args, Instrumentation instrumentation) {System.out.println("進入到premain方法,參數args[" + args + "]");instrumentation.addTransformer(new ClassFileTransformerDemo());}
}

本地實現簡單的TestService類增強:

package cn.git;import javassist.*;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;/*** @description: 類文件轉換器* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class ClassFileTransformerDemo implements ClassFileTransformer {/*** 當字節碼第一次被加載時,會調用該方法* @param className 加載的類的全限定名,包含包名,例如:cn/git/service/TestService/test** @return 需要增強就返回增強后的字節碼,否則返回null*/@Overridepublic byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {// 攔截指定類的字節碼byte[] bytes = null;if ("cn/git/service/TestService".equals(className)) {// 創建新的 ClassPool 實例ClassPool classPool = new ClassPool();// 添加系統類路徑classPool.appendSystemPath();// 添加自定義類路徑classPool.insertClassPath(new LoaderClassPath(loader));CtClass ctClass;try {ctClass = classPool.get("cn.git.service.TestService");CtMethod method = ctClass.getDeclaredMethod("test", new CtClass[]{classPool.get("java.lang.String")});method.insertBefore("{System.out.println(\"hello world\");}");bytes = ctClass.toBytecode();System.out.println("增強代碼成功 class : " + className);} catch (NotFoundException e) {System.out.println("未找到類: " + "cn.git.service.TestService");} catch (Exception e) {e.printStackTrace();System.out.println("獲取類失敗");}}return bytes;}
}

我們還實現了一個簡單的Server端,主要就是一個controller,里面調用了一個testService接口,引入的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><parent><groupId>cn.git</groupId><artifactId>bytebuddy-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>agent-app</artifactId><packaging>jar</packaging><name>agent-app</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.8.RELEASE</version></dependency></dependencies><build><plugins><!-- compiler --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><annotationProcessorPaths><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.6</version></path></annotationProcessorPaths></configuration></plugin><!-- package --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.8.RELEASE</version><executions><execution><goals><goal>repackage</goal></goals></execution></executions><configuration><mainClass>cn.git.Application</mainClass></configuration></plugin></plugins></build>
</project>

controller代碼如下:

package cn.git.controller;import cn.git.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @description: 測試controller* @program: bank-credit-sy* @author: lixuchun* @create: 2024-03-18 03:19:27*/
@RestController
@RequestMapping("/test")
public class TestController {@Autowiredprivate TestService testService;@GetMapping("/testForGet0001/{source}")public String testForGet0001(@PathVariable(value = "source") String source) {System.out.println("獲取到傳入source信息".concat(" : ").concat(source));return testService.test(source);}
}

我們此次要增強的代碼就是此部分,具體的實現如下:

package cn.git.service;import org.springframework.stereotype.Service;@Service
public class TestService {public String test(String id) {return id + " : test";}
}

服務啟動類代碼如下:

package cn.git;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @description: 服務啟動類* @program: bank-credit-sy* @author: lixuchun* @create: 2024-03-15 03:01:52*/
@SpringBootApplication(scanBasePackages = "cn.git")
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

將項目進行打包,打包后的兩個包放到一個文件夾中,然后啟動server服務,訪問接口觀察代碼是否增強:

在這里插入圖片描述

啟動服務腳本:

java -javaagent:.\agent-jdk-1.0-SNAPSHOT-jar-with-dependencies.jar=hello -jar .\agent-app-1.0-SNAPSHOT.jar

在這里插入圖片描述

訪問接口路徑為: http://localhost:8080/test/testForGet0001/jack

在這里插入圖片描述

發現代碼已經被增強:

在這里插入圖片描述

注意:我使用 ClassPool classPool = ClassPool.getDefault(); 這個時候,加載classPool.get()獲取不到taskService類

需要使用如下classPool.insertClassPath(new LoaderClassPath(loader)); 才能獲取到增強類

// 創建新的 ClassPool 實例
ClassPool classPool = new ClassPool();
// 添加系統類路徑
classPool.appendSystemPath();
// 添加自定義類路徑
classPool.insertClassPath(new LoaderClassPath(loader));

3.2 byte buddy實現agent實戰

byte buddy在jdk的java agent基礎上進行了封裝,更加簡單易用。

3.2.1 攔截實例方法

3.2.1.1 注意點
  • AgentBuilder:對java agent常見的類轉換等邏輯進行包裝的構造器類,通常在premain方法入口中使用
  • AgentBuilder.Transformer:對被攔截的類進行修改/增強的轉換器類,這里面主要指定攔截的方法和具體攔截后的增強邏輯
  • AgentBuilder.Listener:監聽器類,在instrumentation過程中執行該類中的hook方法(里面所有類都是hook回調方法,在特定環節被調用,比如某個類被transform后,被ignored后,等等)

其他相關介紹,可見官方教程文檔的"創建Java代理"章節,下面內容摘自官方教程文檔

代碼實現部分,我們還是新增一個instance-method-agent模塊,并且引入對應的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><parent><groupId>cn.git</groupId><artifactId>bytebuddy-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>instance-method-agent</artifactId><packaging>jar</packaging><name>instance-method-agent</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><!-- Byte Buddy --><dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.14.10</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.6</version></dependency></dependencies><build><plugins><plugin><!-- 用于打包插件 --><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.1.0</version><configuration><archive><manifestEntries><!-- MANIFEST.MF 配置項,指定premain方法所在類 --><Premain-Class>cn.git.ByteBuddyAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes><Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix></manifestEntries></archive><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs></configuration><executions><execution><id>make-assembly</id><!-- 什么階段會觸發此插件 --><phase>package</phase><goals><!-- 只運行一次 --><goal>single</goal></goals></execution></executions></plugin></plugins></build></project>

然后我們開始編輯我們的入口方法既premain方法,此處和之前的jdk實現有區別,具體內容如下:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;/*** @description: byteBuddy探針,實現springmvc 攔截器* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class ByteBuddyAgent {/*** 控制器注解名稱* 我們主要攔截的也是這部分編碼*/public static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";public static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";/*** premain方法,main方法執行之前進行調用,插樁代碼入口* @param args 標識外部傳遞參數* @param instrumentation 插樁對象*/public static void premain(String args, Instrumentation instrumentation) {// 創建AgentBuilder對象AgentBuilder builder = new AgentBuilder.Default()// 忽略攔截的包// 當某個類第一次將要加載的時候,會進入到此方法.ignore(ElementMatchers.nameStartsWith("net.bytebuddy").or(ElementMatchers.nameStartsWith("org.apache")))// 攔截標注以什么注解的類.type(ElementMatchers.isAnnotatedWith(ElementMatchers.named(CONTROLLER_NAME).or(ElementMatchers.named(REST_CONTROLLER_NAME))))// 前面的type()方法匹配到的類,進行攔截.transform(new ByteBuddyTransform()).with(new ByteBuddyListener());// 安裝builder.installOn(instrumentation);}
}

ByteBuddyTransform是攔截的具體定義,包含攔截什么方法,以及接口方法不進行攔截等,ByteBuddyTransform具體實現如下所示:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.utility.JavaModule;import java.security.ProtectionDomain;import static net.bytebuddy.matcher.ElementMatchers.*;/*** @description: bytebuddy transform,當被攔截的type第一次要被加載的時候,會進入到此方法* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class ByteBuddyTransform implements AgentBuilder.Transformer {/*** 攔截的注解開頭結尾*/private static final String MAPPING_PACKAGE_PREFIX = "org.springframework.web.bind.annotation";private static final String MAPPING_PACKAGE_SUFFIX = "Mapping";/*** 當被type方法ElementMatcher<? super TypeDescription> 匹配后會進入到此方法** @param builder* @param typeDescription  要被加載的類的信息* @param classLoader      The class loader of the instrumented class. Might be {@code null} to represent the bootstrap class loader.* @param module           The class's module or {@code null} if the current VM does not support modules.* @param protectionDomain The protection domain of the transformed type.* @return A transformed version of the supplied {@code builder}.*/@Overridepublic DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,TypeDescription typeDescription,ClassLoader classLoader,JavaModule module,ProtectionDomain protectionDomain) {// 獲取實際的名字String actualName = typeDescription.getActualName();System.out.println("actualName: " + actualName);// 確保匹配的是具體的類,而不是接口if (typeDescription.isInterface()) {System.out.println("接口不攔截");return builder;}// 實例化 SpringMvcInterceptorSpringMvcInterceptor interceptor = new SpringMvcInterceptor();// 攔截所有被注解標記的方法DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder.method(not(isStatic()).and(isAnnotatedWith(nameStartsWith(MAPPING_PACKAGE_PREFIX).and(nameEndsWith(MAPPING_PACKAGE_SUFFIX))))).intercept(MethodDelegation.to(interceptor));// 不能返回builder,因為bytebuddy里面的庫里面的類基本都是不可變的,修改之后需要返回一個新的builder,避免修改丟失return intercept;}
}

ByteBuddyListener是我們的攔截監聽器, 當接口被攔截增強,或者報錯異常的時候都會觸發監聽,具體代碼實現如下:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.utility.JavaModule;/*** @description: 監聽器* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class ByteBuddyListener implements AgentBuilder.Listener {/*** 當一個類型被發現時調用,就會回調此方法** @param typeName    The binary name of the instrumented type.* @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.* @param module      The instrumented type's module or {@code null} if the current VM does not support modules.* @param loaded      {@code true} if the type is already loaded.*/@Overridepublic void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {if (typeName.contains("TestController")) {System.out.println("onDiscovery: " + typeName);}}/*** 對某一個類型進行轉換時調用,就會回調此方法** @param typeDescription The type that is being transformed.* @param classLoader     The class loader which is loading this type or {@code null} if loaded by the boots loader.* @param module          The transformed type's module or {@code null} if the current VM does not support modules.* @param loaded          {@code true} if the type is already loaded.* @param dynamicType     The dynamic type that was created.*/@Overridepublic void onTransformation(TypeDescription typeDescription,ClassLoader classLoader,JavaModule module,boolean loaded,DynamicType dynamicType) {System.out.println("onTransformation: " + typeDescription.getActualName());}/*** 當某一個類被加載并且被忽略時(包括ignore配置或不匹配)調用,就會回調此方法** @param typeDescription The type being ignored for transformation.* @param classLoader     The class loader which is loading this type or {@code null} if loaded by the boots loader.* @param module          The ignored type's module or {@code null} if the current VM does not support modules.* @param loaded          {@code true} if the type is already loaded.*/@Overridepublic void onIgnored(TypeDescription typeDescription,ClassLoader classLoader,JavaModule module,boolean loaded) {
//        log.info("onIgnored: {}", typeDescription.getActualName());
//        System.out.println("onIgnored: " + typeDescription.getActualName());}/*** 當transform過程中發生異常時,會回調此方法** @param typeName    The binary name of the instrumented type.* @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.* @param module      The instrumented type's module or {@code null} if the current VM does not support modules.* @param loaded      {@code true} if the type is already loaded.* @param throwable   The occurred error.*/@Overridepublic void onError(String typeName,ClassLoader classLoader,JavaModule module,boolean loaded,Throwable throwable) {System.out.println("onError: " + typeName);throwable.printStackTrace();}/*** 當某一個類被處理完,不管是transform還是忽略時,都會回調此方法** @param typeName    The binary name of the instrumented type.* @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.* @param module      The instrumented type's module or {@code null} if the current VM does not support modules.* @param loaded      {@code true} if the type is already loaded.*/@Overridepublic void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onComplete: " + typeName);}
}

我們還是install打包后將兩個包送入到同一目錄下,然后啟動服務:

在這里插入圖片描述

啟動腳本如下:

java -javaagent:.\instance-method-agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\agent-app-1.0-SNAPSHOT.jar.

在這里插入圖片描述

我們訪問接口 http://localhost:8080/test/testForGet0001/jack,發現方法已經被增強

在這里插入圖片描述

在這里插入圖片描述

3.2.2 攔截靜態方法

我們的靜態方法大部分與之前的實例方法一致,比如pom文件,還有server服務,我們的server服務只是在service中新增了一個簡單的靜態方法調用,此處我只標注不一樣的代碼部分。

我們新增一個static-method-agent靜態探針模塊,并且編寫入口premain方法

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;/*** @description: 靜態代理* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class StaticAgentDemo {/*** 攔截className*/public static final String CLASS_NAME = "cn.git.util.StringUtil";/*** premain方法,main方法執行之前進行調用,插樁代碼入口* @param args 標識外部傳遞參數* @param instrumentation 插樁對象*/public static void premain(String args, Instrumentation instrumentation) {System.out.println("進入到premain方法,參數args[" + args + "]");// 創建AgentBuilder對象AgentBuilder builder = new AgentBuilder.Default()// 忽略攔截的包.ignore(ElementMatchers.nameStartsWith("net.bytebuddy").or(ElementMatchers.nameStartsWith("org.apache")))// 當某個類第一次將要加載的時候,會進入到此方法.type(getTypeMatcher())// 前面的type()方法匹配到的類,進行攔截// 靜態方法是在調用的時候進入此邏輯,而spring容器管理類則是初始化就會被加載.transform(new StaticTransformer());// 安裝builder.installOn(instrumentation);}private static ElementMatcher<? super TypeDescription> getTypeMatcher() {// 1. 使用ElementMatchers.named()方法匹配className// return named(CLASS_NAME);// 2. 使用名稱匹配第二種方式return new ElementMatcher<TypeDescription>() {@Overridepublic boolean matches(TypeDescription target) {return CLASS_NAME.equals(target.getActualName());}};}
}

編寫 StaticTransformer 方法,具體代碼實現如下:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.utility.JavaModule;import java.security.ProtectionDomain;import static net.bytebuddy.matcher.ElementMatchers.*;/*** @description: 靜態代理* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class StaticTransformer implements AgentBuilder.Transformer  {/*** Allows for a transformation of a {@link DynamicType.Builder}.** @param builder* @param typeDescription  要被加載的類的信息* @param classLoader      The class loader of the instrumented class. Might be {@code null} to represent the bootstrap class loader.* @param module           The class's module or {@code null} if the current VM does not support modules.* @param protectionDomain The protection domain of the transformed type.* @return A transformed version of the supplied {@code builder}.*/@Overridepublic DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,TypeDescription typeDescription,ClassLoader classLoader,JavaModule module,ProtectionDomain protectionDomain) {// 獲取實際的名字String actualName = typeDescription.getActualName();System.out.println("actualName: " + actualName);// 確保匹配的是具體的類,而不是接口if (typeDescription.isInterface()) {System.out.println("接口不攔截");return builder;}// 攔截所有被注解標記的方法return builder.method(isStatic()).intercept(MethodDelegation.to(new StringUtilInterceptor()));}
}

我們的靜態攔截器類StringUtilInterceptor代碼如下,基本與原有實例攔截器一致,就是@This不能再使用,需要修改為@Origin:

package cn.git;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;public class StringUtilInterceptor {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic Object intercept(@Origin Class<?> targetClass,@Origin Method targetMethod,@AllArguments Object[] targetMethodArgs,@SuperCall Callable<?> superCall) {Long start = System.currentTimeMillis();System.out.println("StaticTargetObject : " + targetClass);System.out.println("StaticTargetMethodName : " + targetMethod.getName());System.out.println("StaticTargetMethodArgs : " + Arrays.toString(targetMethodArgs));Object call;try {call = superCall.call();} catch (Exception e) {e.printStackTrace();throw new RuntimeException(e);} finally {Long end = System.currentTimeMillis();System.out.println(targetMethod.getName() + "  耗時:" + (end - start) + "ms");}return call;}
}

我們在server端則新增了一個util類,cn.git.util.StringUtil ,一個string工具類,里面有一個簡單的拼接方法:

package cn.git.util;/*** @description: 測試用靜態方法類* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-19*/
public class StringUtil {public static String concat(String str, String str2) {return str + "_" + str2;}
}

我們在testService中則是調用了此靜態方法,具體代碼如下:

package cn.git.service;import cn.git.util.StringUtil;
import org.springframework.stereotype.Service;@Service
public class TestService {public String test(String id) {return StringUtil.concat("靜態攔截".concat(String.valueOf(System.currentTimeMillis())), id);}
}

以上便是我們的主要改造部分的具體實現,之后還是編譯成兩個jar包文件,放到一個目錄下,啟動server服務,再次進行接口訪問,觀察是否增強:

啟動腳本如下:

java -javaagent:.\static-method-agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\agent-app-1.0-SNAPSHOT.jar

在這里插入圖片描述

訪問接口路徑地址:http://localhost:8080/test/testForGet0001/jack

在這里插入圖片描述

發現請求接口方法對應靜態方法已經被增強

在這里插入圖片描述

3.2.3 攔截構造器方法

和"2.8 對構造方法進行插樁"區別不大。新建模塊constructor-method-agent,并且引入相同的pom文件,此處不多贅述了。我們需要在app-server端新增一個構造方法,我們選擇在TestService中新增:

package cn.git.service;import cn.git.util.StringUtil;
import org.springframework.stereotype.Service;@Service
public class TestService {/*** 構造方法*/public TestService() {System.out.println("TestService構造方法實例化");}public String test(String id) {return StringUtil.concat("靜態攔截".concat(String.valueOf(System.currentTimeMillis())), id);}
}

我們編輯premain方法,與static靜態方法探針基本相同:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;/*** @description: 構造器攔截探針* @program: bank-credit-sy* @author: lixuchun* @create: 2024-12-20*/
public class ConstructorMethodAgent {/*** 攔截className*/public static final String CLASS_NAME = "cn.git.service.TestService";/*** premain方法,main方法執行之前進行調用,插樁代碼入口* @param args 標識外部傳遞參數* @param instrumentation 插樁對象*/public static void premain(String args, Instrumentation instrumentation) {System.out.println("進入到premain方法,參數args[" + args + "]");// 創建AgentBuilder對象AgentBuilder builder = new AgentBuilder.Default()// 忽略攔截的包.ignore(ElementMatchers.nameStartsWith("net.bytebuddy").or(ElementMatchers.nameStartsWith("org.apache")))// 當某個類第一次將要加載的時候,會進入到此方法.type(getTypeMatcher())// 前面的type()方法匹配到的類,進行攔截.transform(new ConstructorTransformer());// 安裝builder.installOn(instrumentation);}private static ElementMatcher<? super TypeDescription> getTypeMatcher() {// 1. 使用ElementMatchers.named()方法匹配className// return named(CLASS_NAME);// 2. 使用名稱匹配第二種方式return new ElementMatcher<TypeDescription>() {@Overridepublic boolean matches(TypeDescription target) {return CLASS_NAME.equals(target.getActualName());}};}
}

編輯transformer,用于匹配需要增強的構造方法,具體實現如下:

package cn.git;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;import java.security.ProtectionDomain;public class ConstructorTransformer implements AgentBuilder.Transformer {/*** 構造方法進行插樁** @param builder          The dynamic builder to transform.* @param typeDescription  The description of the type currently being instrumented.* @param classLoader      The class loader of the instrumented class. Might be {@code null} to represent the bootstrap class loader.* @param module           The class's module or {@code null} if the current VM does not support modules.* @param protectionDomain The protection domain of the transformed type.* @return A transformed version of the supplied {@code builder}.*/@Overridepublic DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,TypeDescription typeDescription,ClassLoader classLoader,JavaModule module,ProtectionDomain protectionDomain) {System.out.println("ConstructorTransformer開始加載");return builder.constructor(ElementMatchers.any()).intercept(                        // 指定在構造方法執行完畢后再委托給攔截器SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(new ConstructorInterceptor())));}
}

編寫具體增強邏輯的interceptor,具體實現邏輯如下:

package cn.git;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;import java.util.Arrays;public class ConstructorInterceptor {/*** 被標注 RuntimeType 注解的方法就是攔截方法,此時返回的值與返回參數可以與被攔截的方法不一致* byteBuddy會在運行期間給被攔截的方法參數進行賦值* @return*/@RuntimeTypepublic void intercept(@This Object targetObject,@AllArguments Object[] targetMethodArgs) {System.out.println("增強構造方法參數intercept: " + Arrays.toString(targetMethodArgs));}
}

之后我們同樣打包,放置到相同文件夾中,啟動server服務,并且觀察構造方法已經被增強,執行了增強邏輯:

java -javaagent:.\constructor-method-agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\agent-app-1.0-SNAPSHOT.jar

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/63585.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/63585.shtml
英文地址,請注明出處:http://en.pswp.cn/web/63585.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【信息系統項目管理師】高分論文:論信息系統項目的進度管理(人力資源管理系統)

更多內容請見: 備考信息系統項目管理師-專欄介紹和目錄 文章目錄 論文1、規劃進度管理3、排列活動順序4、估算活動持續時間5、制訂進度計劃6、控制進度論文 2022年2月,我作為項目經理參與了XX電網公司人力資源管理系統建設項目,該項目是2022年XX電網公司“十三五”信息化規…

vue3項目結合Echarts實現甘特圖(可拖拽、選中等操作)

效果圖&#xff1a; 圖一&#xff1a;選中操作 圖二&#xff1a;上下左右拖拽操作 本案例在echarts???????示例機場航班甘特圖的基礎上修改??????? 封裝ganttEcharts組件&#xff0c;測試數據 airport-schedule.jsonganttEcharts代碼: 直接復制粘貼可測????…

GIT安裝過程

文章目錄 ?下載安裝包?安裝過程?驗證安裝?Git的基本使用? ?Git的安裝可以通過以下步驟完成? ?下載安裝包? 首先&#xff0c;訪問Git官網(https://git-scm.com/)或Git for Windows(https://gitforwindows.org/)下載對應系統的安裝包。 對于Windows系統&#xff0c;通常…

Java 技術面試常見問題解析

1.說說Mybatis的緩存機制: MyBatis 是一個優秀的持久層框架&#xff0c;它簡化了企業應用開發中數據庫操作的代碼。MyBatis 提供了一級緩存和二級緩存機制來優化對數據庫的訪問。 一級緩存 (SqlSession級別的緩存) 一級緩存是 MyBatis 中默認開啟且無法關閉的緩存機制。它存…

Word使用分隔符實現頁面部分分欄

文章目錄 Word使用分隔符實現頁面部分分欄分隔符使用頁面設置 Word使用分隔符實現頁面部分分欄 分隔符使用 word中的分隔符&#xff1a; 前面不分欄&#xff0c;后面分欄(或前面分欄&#xff0c;后面不分欄)&#xff0c;只需要在分隔位置處插入分隔符&#xff1a;“連續”即…

掌握 Spring Boot 中的 WebClient:何時以及為何使用它而不是 RestTemplate

在開發 Spring Boot 應用程序時&#xff0c;與 RESTful Web 服務進行通信是一項常見需求。從歷史上看&#xff0c;開發人員已將RestTemplate用于此目的。然而&#xff0c;隨著反應式編程的出現和對更高效資源利用的需求&#xff0c;WebClient已成為首選。本文探討了RestTemplat…

主曲率為常數時曲面分類

主曲率為常數 ? K , H \Leftrightarrow K,H ?K,H 為常數&#xff0c;曲面分類&#xff1a; 1.若 k 1 k 2 0 k_1k_20 k1?k2?0,則 S S S為全臍點曲面——平面的一部分&#xff1b; 2.若 k 1 k 2 ≠ 0 k_1k_2\neq0 k1?k2?0,則 S S S為全臍點曲面——球面的一部分&…

asp.net core發布配置端口號,支持linux

方式一&#xff0c;修改配置文件 appsettings.json 找到文件 appsettings.json&#xff0c; 添加如下節點配置&#xff0c;在linux環境需要設置0.0.0.0才可以正常代表本機&#xff0c;然后被其他機器訪問&#xff0c;此處設置端口8000&#xff0c; "Kestrel": {&quo…

【安當產品應用案例100集】033-安當TDE透明加密在移動存儲加密中的應用案例

背景介紹 隨著移動互聯網的普及&#xff0c;企業和個人越來越依賴移動存儲設備&#xff0c;如U盤、移動硬盤以及云存儲服務進行數據的存儲和傳輸。然而&#xff0c;這種便捷性也帶來了數據安全的隱患。如何確保存儲在移動設備上的數據不被非法訪問和泄露&#xff0c;成為企業和…

【linux 內存】cat /proc/meminfo、free

cat /proc/meminfo 各字段詳解 /proc/meminfo是了解Linux系統內存使用狀況的主要接口&#xff0c;我們最常用的”free”、”vmstat”等命令就是通過它獲取數據的 &#xff0c;/proc/meminfo所包含的信息比”free”等命令要豐富得多&#xff0c;因此需要了解這些字段的含義。 …

Android HandlerThread、Looper、MessageQueue 源碼分析

Android HandlerThread、Looper、MessageQueue 源碼分析 簡介 在 Android 開發中&#xff0c;大家應該對 HandlerThread 有一定了解。顧名思義&#xff0c;HandlerThread 是 Thread 的一個子類。與普通的 Thread 不同&#xff0c;Thread 通常一次只能執行一個后臺任務&#x…

配置PostgreSQL用于集成測試的步驟

在進行軟件開發時&#xff0c;集成測試是確保各個組件能夠協同工作的關鍵環節。PostgreSQL作為一種強大的開源數據庫系統&#xff0c;常被用于集成測試中。下面將詳細介紹如何在不同的環境中配置PostgreSQL以支持集成測試。 1. 選擇并安裝PostgreSQL 首先&#xff0c;你需要根…

WebRTC搭建與應用(一)-ICE服務搭建

WebRTC搭建與應用(一) 近期由于項目需要在研究前端WebGL渲染轉為云渲染&#xff0c;借此機會對WebRTC、ICE信令協議等有了初步了解&#xff0c;在此記錄一下&#xff0c;以防遺忘。 第一章 ICE服務搭建 文章目錄 WebRTC搭建與應用(一)前言一、ICE是什么&#xff1f;二、什么…

【學習筆記】深入淺出詳解Pytorch中的View, reshape, unfold,flatten等方法。

文章目錄 一、寫在前面二、Reshape&#xff08;一&#xff09;用法&#xff08;二&#xff09;代碼展示 三、Unfold&#xff08;一&#xff09;torch.unfold 的基本概念&#xff08;二&#xff09;torch.unfold 的工作原理&#xff08;三&#xff09; 示例代碼&#xff08;四&a…

深入理解 MySQL 索引

引言 在數據庫管理中&#xff0c;索引&#xff08;Index&#xff09;是提高查詢性能的關鍵技術之一。MySQL 是最流行的關系型數據庫管理系統之一&#xff0c;廣泛應用于各種規模的應用程序中。本文將深入探討 MySQL 中的索引概念、類型、工作原理以及最佳實踐&#xff0c;幫助…

利用notepad++刪除特定關鍵字所在的行

1、按組合鍵Ctrl H&#xff0c;查找模式選擇 ‘正則表達式’&#xff0c;不選 ‘.匹配新行’ 2、查找目標輸入 &#xff1a; ^.*關鍵字.*\r\n (不保留空行) ^.*關鍵字.*$ (保留空行)3、替換為&#xff1a;&#xff08;空&#xff09; 配置界面參考下圖&#xff1a; ??…

docker安裝和換源

安裝&#xff1a; https://www.runoob.com/docker/ubuntu-docker-install.html sudo apt-get remove docker docker-engine docker.io containerd runcsudo apt-get install \apt-transport-https \ca-certificates \curl \gnupg-agent \software-properties-commoncurl -fsS…

CSSmodule的作用是什么

CSS Modules的作用主要體現在以下幾個方面&#xff1a; 1. 解決全局樣式污染問題 在傳統的CSS管理方式中&#xff0c;樣式定義通常是全局的&#xff0c;這很容易導致全局樣式污染。當多個組件或頁面共享同一個樣式時&#xff0c;可能會出現樣式沖突和覆蓋的情況&#xff0c;從…

創建第一個QML項目

文章目錄 使用 Qt Creator 創建 Qt Quick 項目詳解為什么選擇 Qt Creator&#xff1f;1. 打開 Qt Creator2. 選擇項目模板3. 設置項目名稱與路徑4. 定義項目細節5. 配置構建套件6. 檢查項目配置7. 編譯并運行項目后續操作修改界面添加功能 總結 使用 Qt Creator 創建 Qt Quick …

【k8s集群應用】K8S二進制安裝大致步驟(簡略版)

文章目錄 K8S二進制安裝部署etcd測試etcd集群&#xff08;可選&#xff09;恢復etcd數據庫 部署master組件部署node組件K8S kubeadm安裝關鍵命令更新kubeadm安裝的K8S證書有效期方法一方法二查看證書有效期 K8S二進制安裝 部署etcd 使用cfssl工具簽發證書和私鑰下載解壓etcd軟…