网站模板库 下载,离线网站制作,鞍山便民网,公司做自己的网站简单的bytebuddy学习笔记
此笔记对应b站bytebuddy学习视频进行整理#xff0c;此为视频地址#xff0c;此处为具体的练习代码地址
一、简介
ByteBuddy是基于ASM (ow2.io)实现的字节码操作类库。比起ASM#xff0c;ByteBuddy的API更加简单易用。开发者无需了解class file …
简单的bytebuddy学习笔记
此笔记对应b站bytebuddy学习视频进行整理此为视频地址此处为具体的练习代码地址
一、简介
ByteBuddy是基于ASM (ow2.io)实现的字节码操作类库。比起ASMByteBuddy的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文件如下
dependencyManagementdependencies!-- 单元测试 --dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversionRELEASE/versionscopetest/scope/dependency!-- Byte Buddy --dependencygroupIdnet.bytebuddy/groupIdartifactIdbyte-buddy/artifactIdversion1.14.10/version/dependency!-- 工具类 --dependencygroupIdcommons-io/groupIdartifactIdcommons-io/artifactIdversion2.15.0/version/dependency/dependencies
/dependencyManagement测试模块对应pom引入包
dependencies!-- 单元测试 --dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdscopetest/scope/dependency!-- Byte Buddy --dependencygroupIdnet.bytebuddy/groupIdartifactIdbyte-buddy/artifactId/dependency!-- 工具类 --dependencygroupIdcommons-io/groupIdartifactIdcommons-io/artifactId/dependency
/dependencies2.1 生成一个类
2.1.1 注意点 Byte Buddy默认命名策略(NamingStrategy)生成的类名 父类为jdk自带类: net.bytebuddy.renamed.{超类名}$ByteBuddy${随机字符串}父类非jdk自带类 {超类名}$ByteBuddy${随机字符串} 如果自定义命名策略官方建议使用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.UnloadedUserManager 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.UnloadedUserManager 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.LoadedUserManager 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.UnloadedUserManager 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.UnloadedUserManager 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.UnloadedUserManager 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对于返回原始类型的方法注入0FieldValue注入被拦截对象的一个字段的值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.UnloadedUserManager 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.LoadedUserManager 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 superObjectSuper Object superObject,// 用于调用目标方法SuperCall Callable? superCall) {// cn.git.budy.test.BuddyUserManagera1f72f5System.out.println(targetObject : targetObject);// selectUserNameSystem.out.println(targetMethodName : targetMethod.getName());// [1521]System.out.println(targetMethodArgs : Arrays.toString(targetMethodArgs));// cn.git.budy.test.BuddyUserManagera1f72f5System.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.UnloadedUserManager 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.LoadedUserManager 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 methods 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.UnloadedUserManager 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.LoadedUserManager 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.UnloadedUserManager 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.LoadedUserManager 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.BuddyUserManagera1f72f5System.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.UnloadedFileUtils 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.LoadedFileUtils 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以目标类子类的形式重写父类方法完成修改/增强。子类不能重写静态方法所以增强目标类的静态方法时不能用subclassredefine因为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.UnloadedSomethingClass实例.load(getClass().getClassLoader()).getLoaded()等同于DynamicType.UnloadedSomethingClass实例.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-namebinary name/a. The* default implementation of this method searches for classes in the* following order:** param name* The a href#binary-namebinary 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.UnloadedObject 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.UnloadedObject 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.INSTANCEByte 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.UnloadedUserManager 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 xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdcn.git/groupIdartifactIdbytebuddy-demo/artifactIdversion1.0-SNAPSHOT/version/parentartifactIdagent-jdk/artifactIdpackagingjar/packagingnameagent-jdk/nameurlhttp://maven.apache.org/urlpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncoding/propertiesdependenciesdependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion3.8.1/versionscopetest/scope/dependencydependencygroupIdorg.javassist/groupIdartifactIdjavassist/artifactIdversion3.28.0-GA/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.6/version/dependency/dependenciesbuildpluginsplugin!-- 用于打包插件 --groupIdorg.apache.maven.plugins/groupIdartifactIdmaven-assembly-plugin/artifactIdversion3.1.0/versionconfigurationarchivemanifestEntries!-- MANIFEST.MF 配置项,指定premain方法所在类 --Premain-Classcn.git.AgentDemo/Premain-ClassCan-Redefine-Classestrue/Can-Redefine-ClassesCan-Retransform-Classestrue/Can-Retransform-ClassesCan-Set-Native-Method-Prefixtrue/Can-Set-Native-Method-Prefix/manifestEntries/archivedescriptorRefsdescriptorRefjar-with-dependencies/descriptorRef/descriptorRefs/configurationexecutionsexecutionidmake-assembly/id!-- 什么阶段会触发此插件 --phasepackage/phasegoals!-- 只运行一次 --goalsingle/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 xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdcn.git/groupIdartifactIdbytebuddy-demo/artifactIdversion1.0-SNAPSHOT/version/parentartifactIdagent-app/artifactIdpackagingjar/packagingnameagent-app/nameurlhttp://maven.apache.org/urlpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncoding/propertiesdependenciesdependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion3.8.1/versionscopetest/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactIdversion2.3.8.RELEASE/version/dependency/dependenciesbuildplugins!-- compiler --plugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.8.1/versionconfigurationsource1.8/sourcetarget1.8/targetannotationProcessorPathspathgroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.6/version/path/annotationProcessorPaths/configuration/plugin!-- package --plugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdversion2.3.8.RELEASE/versionexecutionsexecutiongoalsgoalrepackage/goal/goals/execution/executionsconfigurationmainClasscn.git.Application/mainClass/configuration/plugin/plugins/build
/projectcontroller代码如下
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.jarhello -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 xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdcn.git/groupIdartifactIdbytebuddy-demo/artifactIdversion1.0-SNAPSHOT/version/parentartifactIdinstance-method-agent/artifactIdpackagingjar/packagingnameinstance-method-agent/nameurlhttp://maven.apache.org/urlpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncoding/propertiesdependenciesdependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion3.8.1/versionscopetest/scope/dependency!-- Byte Buddy --dependencygroupIdnet.bytebuddy/groupIdartifactIdbyte-buddy/artifactIdversion1.14.10/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.6/version/dependency/dependenciesbuildpluginsplugin!-- 用于打包插件 --groupIdorg.apache.maven.plugins/groupIdartifactIdmaven-assembly-plugin/artifactIdversion3.1.0/versionconfigurationarchivemanifestEntries!-- MANIFEST.MF 配置项,指定premain方法所在类 --Premain-Classcn.git.ByteBuddyAgent/Premain-ClassCan-Redefine-Classestrue/Can-Redefine-ClassesCan-Retransform-Classestrue/Can-Retransform-ClassesCan-Set-Native-Method-Prefixtrue/Can-Set-Native-Method-Prefix/manifestEntries/archivedescriptorRefsdescriptorRefjar-with-dependencies/descriptorRef/descriptorRefs/configurationexecutionsexecutionidmake-assembly/id!-- 什么阶段会触发此插件 --phasepackage/phasegoals!-- 只运行一次 --goalsingle/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 classs 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 types 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 types 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 types 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 types 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 types 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 ElementMatcherTypeDescription() {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 classs 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 ElementMatcherTypeDescription() {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 classs 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