简单的bytebuddy学习笔记
简单的bytebuddy学习笔记
此笔记对应b站bytebuddy学习视频进行整理,此为视频地址,此处为具体的练习代码地址
一、简介
ByteBuddy是基于ASM (ow2.io)实现的字节码操作类库。比起ASM,ByteBuddy的API更加简单易用。开发者无需了解class file format知识,也可通过ByteBuddy完成字节码编辑。
- ByteBuddy使用java5实现,并且支持生成JDK6及以上版本的字节码(由于jdk6和jdk7使用未加密的HTTP类库, 作者建议至少使用jdk8版本)
- 和其他字节码操作类库一样,ByteBuddy支持生成类和修改现存类
- 与与静态编译器类似,需要在快速生成代码和生成快速的代码之间作出平衡,ByteBuddy主要关注以最少的运行时间生成代码
Byte Buddy - runtime code generation for the Java virtual machine
JIT优化后的平均ns纳秒耗时(标准差) | 基线 | Byte 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
相关文章:

简单的bytebuddy学习笔记
简单的bytebuddy学习笔记 此笔记对应b站bytebuddy学习视频进行整理,此为视频地址,此处为具体的练习代码地址 一、简介 ByteBuddy是基于ASM (ow2.io)实现的字节码操作类库。比起ASM,ByteBuddy的API更加简单易用。开发者无需了解class file …...

【服务端】Redis 内存超限问题的深入探讨
在 Java 后端开发中,Redis 内存超限是一个常见的问题,可能由多种原因引起。理解这些原因以及如何处理已经超出限制的数据对于保持系统的稳定性和性能至关重要。 一、Redis 内存超限的原因分析 Redis 是一个高性能的内存键值对存储系统,它在…...

Springboot logback 日志打印配置文件,每个日志文件100M,之后滚动到下一个日志文件,日志保留30天(包含traceid)
全部配置 logback.xml <?xml version"1.0" encoding"UTF-8"?> <configuration debug"false"><property name"LOG_HOME" value"log"/><property name"LOG_NAME" value"admin"/&g…...

《计算机组成及汇编语言原理》阅读笔记:p1-p8
《计算机组成及汇编语言原理》学习第 1 天,p1-p8 总结,总计 8 页。 一、技术总结 1.Intel 8088 microprocessor(微处理器), 1979-1988。 2.MS-DOS Microsoft Disk Operating System的缩写,是一个操作系统(operating system)。…...

【游戏中orika完成一个Entity的复制及其Entity异步落地的实现】 1.ctrl+shift+a是飞书下的截图 2.落地实现
一、orika工具使用 1)工具类 package com.xinyue.game.utils;import ma.glasnost.orika.MapperFactory; import ma.glasnost.orika.impl.DefaultMapperFactory;/*** author 王广帅* since 2022/2/8 22:37*/ public class XyBeanCopyUtil {private static MapperFactory mappe…...

在 Ubuntu 上安装 MySQL 的详细指南
在Ubuntu环境中安装 mysql-server 以及 MySQL 开发包(包括头文件和动态库文件),并处理最新版本MySQL初始自动生成的用户名和密码,可以通过官方的APT包管理器轻松完成。以下是详细的步骤指南,包括从官方仓库和MySQL官方…...

Java 优化springboot jar 内存 年轻代和老年代的比例 减少垃圾清理耗时 如调整 -XX:NewRatio
-XX:NewRatio 是 Java Virtual Machine (JVM) 的一个选项,用于调整 年轻代(Young Generation)和 老年代(Old Generation)之间的内存比例。 1. 含义 XX:NewRatioN 用于指定 老年代 与 年轻代 的内存比例。 N 的含义&…...

嵌入式驱动RK3566 HDMI eDP MIPI 背光 屏幕选型与调试提升篇-eDP屏
eDP是嵌入式显示端口,具有高数据传输速率,高带宽,高分辨率、高刷新率、低电压、简化接口数量等特点。现大多数笔记本电脑都是用的这种接口。整个eDP是很复杂的,这里我们不讲底层原理,我们先掌握如何用泰山派来驱动各种…...

在Java虚拟机(JVM)中,方法可以分为虚方法和非虚方法。
在Java虚拟机(JVM)中,方法可以分为虚方法和非虚方法。以下是关于这两种方法的详细解释: 一、虚方法(Virtual Method) 定义:虚方法是指在运行时由实例的实际类型决定的方法。在Java中,所有的非私有、非静态、非final方法都是虚方法。当调用一个虚方法时,JVM会根据实…...

【windows】sonarqube起不来的问题解决
1. 现象与本质 因JDK的问题(比如版本太低或者太高,推荐JDK17)或者其他环境因素,导致sonarqube启动后自动关闭了。 从日志来看,根本看不出来什么,只有警告,没有ERROR,警告也不是本质问题&#…...

golang异常
panic如果不处理会导致应用进程挂掉 defer recover可以处理这种情况 一个recover只处理自己协程 产生panic的情况 空指针 数组越界 空map中添加键值对 错误,error接口,不严重 error.wrapof解决嵌套问题或者error.unwrap erroe.is方法,判断是…...

搭建MongoDB
title: 搭建MongoDB date: 2024-11-30 23:30:00 categories: - 服务器 tags: - MongoDB - 大数据搭建MongoDB 环境:Centos 7-2009 1. 创建MongoDB的国内yum源 # 下载Centos7对应最新版7.0.15的安装包 cat >> /etc/yum.repos.d/mongodb.repo << &quo…...

Android中坐标体系知识超详细讲解
说来说去都不如画图示意简单易懂啊!!!真是的! 来吧先上张图! (一)首先明确一下android 中的坐标系统: 屏幕的左上角是坐标系统原点(0,0) 原点向右延伸是X轴正…...

不需要服务器,使用netlify快速部署自己的网站
Netlify简介 1.1 Netlify的功能与特点 Netlify 是一个功能强大的静态网站托管平台,它不仅提供了简单的网站部署功能,还集成了许多现代化的开发工具和服务,帮助开发者更高效地构建、部署和管理网站。Netlify 的核心功能包括: 自动…...

Swin transformer 论文阅读记录 代码分析
该篇文章,是我解析 Swin transformer 论文原理(结合pytorch版本代码)所记,图片来源于源paper或其他相应博客。 代码也非原始代码,而是从代码里摘出来的片段,配上简单数据,以便理解。 当然&…...

信息安全概论
文章目录 预测题重要考点1.遇到什么威胁有什么漏洞怎么缓解分析题2.网络安全现状分析 2.网络安全亮点 时间信息安全概论期末简答题软件学院实验室服务器安全风险分析与PDRR策略 1.1 信息时代的特点1.2 信息安全威胁1.3信息安全趋势1.4 研究网络与信息安全的意义2.1安全风险分析…...

2024年12月16日Github流行趋势
项目名称:PDFMathTranslate 项目维护者:Byaidu reycn hellofinch Wybxc YadominJinta项目介绍:基于 AI 完整保留排版的 PDF 文档全文双语翻译,支持 Google/DeepL/Ollama/OpenAI 等服务,提供 CLI/GUI/Docker。项目star数…...

Go 1.24即将到来!
Go 1.24 尚未发布。以下是正在撰写中的发布说明,预计 Go 1.24 将于 2025 年 2 月发布。 语言改进 Go 1.24 现在全面支持 泛型类型别名:类型别名可以像定义类型一样被参数化。详情请参阅语言规范。目前,可通过设置 GOEXPERIMENTnoaliastypep…...

FFmpeg库之ffplay
文章目录 FFmpeg环境搭建ffplay使用通用选项视频选项音频选项快捷键使用滤镜直播拉流 FFmpeg环境搭建 FFmpeg官网 FFmpeg环境搭建 ./configure \--prefix"$HOME/ffmpeg" \--extra-cflags"-I$HOME/ffmpeg/include" \--extra-ldflags"-L$HOME/ffmpeg…...

scala中模式匹配的应用
package test34object test6 {case class Person(name:String)case class Student(name:String, className:String)// match case 能根据 类名和属性的信息,匹配到对应的类// 注意:// 1 匹配的时候,case class的属性个数要对上// 2 属性名不需…...

WebRTC搭建与应用(一)-ICE服务搭建
WebRTC搭建与应用(一) 近期由于项目需要在研究前端WebGL渲染转为云渲染,借此机会对WebRTC、ICE信令协议等有了初步了解,在此记录一下,以防遗忘。 第一章 ICE服务搭建 文章目录 WebRTC搭建与应用(一)前言一、ICE是什么?二、什么…...

【计算机视觉基础CV】03-深度学习图像分类实战:鲜花数据集加载与预处理详解
本文将深入介绍鲜花分类数据集的加载与处理方式,同时详细解释代码的每一步骤并给出更丰富的实践建议和拓展思路。以实用为导向,为读者提供从数据组织、预处理、加载到可视化展示的完整过程,并为后续模型训练打下基础。 前言 在计算机视觉的深…...

Kafka学习篇
Architecture 系统间解耦,异步通信,削峰填谷 Topic 消息主题,用于存储消息 Partition 分区,通过扩大分区,可以提高存储量 Broker 部署Kafka服务的设备 Leader kafka主分区 Follwer kafka从分区 高性能之道:…...

冬日养仓鼠小指南:温暖与陪伴同行
随着冬日的脚步悄然来临,家中可爱的小仓鼠也需要我们给予更多的关怀与呵护。仓鼠虽小,但它们的冬日养护却大有学问,关乎着这些小生命能否健康快乐地度过寒冷季节。 保暖是冬季养仓鼠的首要任务。我们可以为仓鼠的小窝增添一些保暖材料&#…...

【计算机视觉基础CV】05 - 深入解析ResNet与GoogLeNet:从基础理论到实际应用
引言 在上一篇文章中,我们详细介绍了ResNet与GoogLeNet的网络结构、设计理念及其在图像分类中的应用。本文将继续深入探讨如何在实际项目中应用这些模型,特别是如何保存训练好的模型、加载模型以及使用模型进行新图像的预测。通过这些步骤,读…...

Python爬虫之代理的设置
【1】urllib中使用公开代理 import urllib.requesturl http://www.baidu.com/s?wdipheaders {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 }# 请求对象的定制 request urllib.req…...

Canoe E2E校验自定义Checksum算法
文章目录 一、添加 DBC文件二、导入要仿真的ECU节点三、编写 CAPL脚本1. 创建 .can 文件2. 设置counter递增3. 设置 CRC 算法,以profile01 8-bit SAE J1850 CRC校验为例 四、开始仿真五、运行结果CRC在线校验 当E2E的 CRC算法非常规算法,则需要自己编写代…...

[HNCTF 2022 Week1]你想学密码吗?
下载附件用记事本打开 把这些代码放在pytho中 # encode utf-8 # python3 # pycryptodemo 3.12.0import Crypto.PublicKey as pk from hashlib import md5 from functools import reducea sum([len(str(i)) for i in pk.__dict__]) funcs list(pk.__dict__.keys()) b reduc…...

端到端自动驾驶大模型:视觉-语言-动作模型 VLA
模型框架定义、模型快速迭代能力是考查智驾团队出活能力的两个核心指标。在展开讨论Vision-Language-Action Models(VLA)之前,咱们先来讨论端到端自动驾驶大模型设计。 目录 1. 端到端自动驾驶大模型设计 1.1 模型输入设计 1.2 模型输出设计 1.3 实现难点分析 …...

druid与pgsql结合踩坑记
最近项目里面突然出现一个怪问题,数据库是pgsql,jdbc连接池是alibaba开源的druid,idea里面直接启动没问题,打完包放在centos上和windows上cmd窗口都能直接用java -jar命令启动,但是放到国产信创系统上就是报错…...