简单的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 Buddy | cglib | Javassist | Java 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 invocation | 0.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 注意点
-
Byte Buddy默认命名策略(NamingStrategy),生成的类名
- 父类为jdk自带类:
net.bytebuddy.renamed.{超类名}$ByteBuddy${随机字符串}
- 父类非jdk自带类
{超类名}$ByteBuddy${随机字符串}
- 父类为jdk自带类:
-
如果自定义命名策略,官方建议使用Byte Buddy内置的
NamingStrategy.SuffixingRandom
-
Byte Buddy本身有对生成的字节码进行校验的逻辑,可通过
.with(TypeValidation.of(false))
关闭 -
.subclass(XXX.class)
指定超类(父类) -
.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
不可用rebase
和redefine
都可以修改目标类静态方法,但是若想在原静态方法逻辑基础上增加其他增强逻辑,那么只有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${随机字符串}
- 超类为jdk自带类:
.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.Foo
和example.Bar
两个类, 如果我们将example.Foo
注入一个已经存在的类加载器,这个类加载器可能会尝试定位查找example.Bar
。 然而,这个查找会失败,因为后一个类是动态创建的,而且对于刚才注入example.Foo
类的类加载器来说是不可达的。 因此反射的方法不能用于在类加载期间生效的带有循环依赖的类。幸运的是,大多数JVM的实现在第一次使用时都会延迟解析引用类, 这就是类注入通常在没有这些限制的时候正常工作的原因。此外,实际上,由 Byte Buddy 创建的类通常不会受这样的循环影响。
你可能会任务遇到循环依赖的机会是无关紧要的,因为一次只创建一个动态类。然而,动态类型的创建可能会触发辅助类型的创建。 这些类型由 Byte Buddy 自动创建,以提供对正在创建的动态类型的访问。我们将在下面的章节学习辅助类型,现在不要担心这些。 但是,正因为如此,我们推荐你尽可能通过创建一个特定的ClassLoader
来加载动态类, 而不是将他们注入到一个已存在的类加载器。
创建一个DynamicType.Unloaded
后,这个类型可以用ClassLoadingStrategy
加载。 如果没有提供这个策略,Byte Buddy 会基于提供的类加载器推测出一种策略,并且仅为启动类加载器创建一个新的类加载器, 该类加载器不能用反射的方式注入任何类。否则为默认设置。
Byte Buddy 提供了几种开箱即用的类加载策略, 每一种都遵循上述概念中的其中一个。这些策略都在ClassLoadingStrategy.Default
中定义,其中, WRAPPER
策略会创建一个新的,经过包装的ClassLoader
, CHILD_FIRST
策略会创建一个类似的具有孩子优先语义的类加载器,INJECTION
策略会用反射注入一个动态类型。
WRAPPER
和CHILD_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默认的拦截器方法实现之一,会根据被拦截的目标方法的返回值类型返回对应的默认值
- The value 0 for all numeric type.
- The null character for the char type.
- false for the boolean type.
- Nothing for void types.
- A null reference for any reference types. Note that this includes primitive wrapper types.
- 当使用
ElementMatchers.any()
时,仅subclass
包含构造方法,rebase
和redefine
不包含构造方法 - 使用
ElementMatchers.any().and(ElementMatchers.isDeclaredBy(目标类.class))
时,仅subclass
支持修改生成类名,rebase
和redefine
若修改类名则拦截后的修改/增强逻辑无效。
演示代码:
/*** 清空方法体,起到保护源码的作用*/
@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