简单的bytebuddy学习笔记

news2024/12/21 17:16:02

在这里插入图片描述

简单的bytebuddy学习笔记

此笔记对应b站bytebuddy学习视频进行整理,此为视频地址,此处为具体的练习代码地址

一、简介

ByteBuddy是基于ASM (ow2.io)实现的字节码操作类库。比起ASM,ByteBuddy的API更加简单易用。开发者无需了解class file format知识,也可通过ByteBuddy完成字节码编辑。

  • ByteBuddy使用java5实现,并且支持生成JDK6及以上版本的字节码(由于jdk6和jdk7使用未加密的HTTP类库, 作者建议至少使用jdk8版本)
  • 和其他字节码操作类库一样,ByteBuddy支持生成类和修改现存类
  • 与与静态编译器类似,需要在快速生成代码和生成快速的代码之间作出平衡,ByteBuddy主要关注以最少的运行时间生成代码

Byte Buddy - runtime code generation for the Java virtual machine

JIT优化后的平均ns纳秒耗时(标准差)基线Byte BuddycglibJavassistJava proxy
普通类创建0.003 (0.001)142.772 (1.390)515.174 (26.753)193.733 (4.430)70.712 (0.645)
接口实现0.004 (0.001)1’126.364 (10.328)960.527 (11.788)1’070.766 (59.865)1’060.766 (12.231)
stub方法调用0.002 (0.001)0.002 (0.001)0.003 (0.001)0.011 (0.001)0.008 (0.001)
类扩展0.004 (0.001)885.983 5’408.329 (7.901) (52.437)1’632.730 (52.737)683.478 (6.735)
super method invocation0.004 (0.001)0.004 0.004 (0.001) (0.001)0.021 (0.001)0.025 (0.001)

上表通过一些测试,对比各种场景下,不同字节码生成的耗时。对比其他同类字节码生成类库,Byte Buddy在生成字节码方面整体耗时还是可观的,并且生成后的字节码运行时耗时和基线十分相近。

  • Java 代理

    Java 类库自带的一个代理工具包,它允许创建实现了一组给定接口的类。这个内置的代理很方便,但是受到的限制非常多。 例如,上面提到的安全框架不能以这种方式实现,因为我们想要扩展类而不是接口。

  • cglib

    代码生成库是在 Java 开始的最初几年实现的,不幸的是,它没有跟上 Java 平台的发展。尽管如此,cglib仍然是一个相当强大的库, 但它是否积极发展变得很模糊。出于这个原因,许多用户已不再使用它。

    (cglib目前已不再维护,并且github中也推荐开发者转向使用Byte Buddy)

  • Javassist

    该库带有一个编译器,该编译器采用包含 Java 源码的字符串,这些字符串在应用程序运行时被翻译成 Java 字节码。 这是非常雄心勃勃的,原则上是一个好主意,因为 Java 源代码显然是描述 Java 类的非常的好方法。但是, Javassist 编译器在功能上无法与 javac 编译器相比,并且在动态组合字符串以实现更复杂的逻辑时容易出错。此外, Javassist 带有一个代理库,它类似于 Java 的代理程序,但允许扩展类并且不限于接口。然而, Javassist 代理工具的范围在其API和功能方面同样受限限制。

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

二、常用API

我们操作需要先引入对应的pom文件如下:

<dependencyManagement>
    <dependencies>
        <!-- 单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>

        <!-- Byte Buddy -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.14.10</version>
        </dependency>

        <!-- 工具类 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.15.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

测试模块对应pom引入包:

<dependencies>
    <!-- 单元测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Byte Buddy -->
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
    </dependency>

    <!-- 工具类 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
    </dependency>
</dependencies>

2.1 生成一个类

2.1.1 注意点

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

    1. 父类为jdk自带类: net.bytebuddy.renamed.{超类名}$ByteBuddy${随机字符串}
    2. 父类非jdk自带类 {超类名}$ByteBuddy${随机字符串}
  2. 如果自定义命名策略,官方建议使用Byte Buddy内置的NamingStrategy.SuffixingRandom

  3. Byte Buddy本身有对生成的字节码进行校验的逻辑,可通过.with(TypeValidation.of(false))关闭

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

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

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

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

2.1.2 示例代码

package cn.git;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

/**
 * @description: bytebuddy测试类
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-18
 */
public class ByteBuddyTest {

    /**
     * 生成文件目录
     */
    private String path;

    @Before
    public void init() {
        // /D:/idea_workspace/bytebuddy-demo/bytebuddy-demo/bytebuddy-test/target/test-classes/
        path = ByteBuddyTest.class.getClassLoader().getResource("").getPath();
        System.out.println(path);
    }

    @Test
    public void testCreateClass() throws IOException {
        // 指定命名策略,生成名称:UserManager$roadJava$aWAN65zL.class
        // 非指定生成名称:UserManager$ByteBuddy$A7LQLGil.class
        NamingStrategy.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.jar
        unloaded.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继承自DynamicType
    DynamicType.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继承自DynamicType
    DynamicType.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
     */
    @RuntimeType
    public 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@a1f72f5
        System.out.println("targetObject : " + targetObject);
        // selectUserName
        System.out.println("targetMethodName : " + targetMethod.getName());
        // [1521]
        System.out.println("targetMethodArgs : " + Arrays.toString(targetMethodArgs));
        // cn.git.budy.test.BuddyUserManager@a1f72f5
        System.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;
    }
}

主方法代码为:

/**
     * 方法委托,使用自己自定义的拦截器
     */
    @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))
                // 不同签名的方法
                .intercept(MethodDelegation.to(new UserManagerDiffMethodNameInterceptor()))
                .make();

        // loaded表示字节码已经加载到jvm中
        // loaded同样拥有saveIn,getBytes,inject方法,unloaded继承自DynamicType
        DynamicType.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
     */
    @RuntimeType
    public 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继承自DynamicType
    DynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());
    // 获取加载的类
    Class<? extends UserManager> loadClass = loaded.getLoaded();

    // 创建实例调用实例方法,预期结果 101
    UserManager 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
     */
    @RuntimeType
    public 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继承自DynamicType
    DynamicType.Loaded<UserManager> loaded = unloaded.load(getClass().getClassLoader());
    // 获取加载的类
    Class<? extends UserManager> loadClass = loaded.getLoaded();

    // 创建实例调用实例方法,预期结果 101
    UserManager 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
     */
    @RuntimeType
    public Object diffNameMethod(
            // 被拦截的目标对象,静态方法只能拿取到class类对象,拿取不到This对象
            @Origin Class<?> targetClass,
            // 被拦截的目标方法,拦截实例方法以及静态方法有效
            @Origin Method targetMethod,
            // 被拦截的目标方法参数,拦截实例方法以及静态方法有效
            @AllArguments Object[] targetMethodArgs,
            // 用于调用目标方法
            @SuperCall Callable<?> superCall) {
        // cn.git.budy.test.BuddyUserManager@a1f72f5
        System.out.println("targetClass : " + targetClass);
        // selectUserName
        System.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继承自DynamicType
    DynamicType.Loaded<FileUtils> loaded = unloaded.load(getClass().getClassLoader());
    // 获取加载的类
    Class<? extends FileUtils> loadClass = loaded.getLoaded();
    Method sizeOfMethod = loadClass.getMethod("sizeOf", File.class);
    Object fileSize = sizeOfMethod.invoke(null, new File("D:\\SubObj.class"));
    System.out.println(fileSize.toString());
    unloaded.saveIn(new File(path));
}

调用结果展示如下:

在这里插入图片描述

2.10 @SuperCall, rebase, redefine, subclass

2.10.1 注意点

  • @SuperCall仅在原方法仍存在的场合能够正常使用,比如subclass超类方法仍为目标方法,而rebase则是会重命名目标方法并保留原方法体逻辑;但redefine直接替换掉目标方法,所以@SuperCall不可用
  • rebaseredefine都可以修改目标类静态方法,但是若想在原静态方法逻辑基础上增加其他增强逻辑,那么只有rebase能通过@SuperCall@Morph调用到原方法逻辑;redefine不保留原目标方法逻辑

2.10.2 示例代码

这里使用的示例代码和"2.9.2 示例代码"一致,主要是用于说明前面"2.9 对静态方法进行插桩"时为什么只能用rebase,而不能用subclass;以及使用rebase后,整个增强的大致调用流程。

  • subclass:以目标类子类的形式,重写父类方法完成修改/增强。子类不能重写静态方法,所以增强目标类的静态方法时,不能用subclass
  • redefine:因为redefine不保留目标类原方法,所以UserManagerStatic中的diffNameMethod方法获取不到@SuperCall Callable<?> superCall参数,若注解掉superCall相关的代码,发现能正常运行,但是目标方法相当于直接被替换成我们的逻辑,达不到保留原方法逻辑并增强的目的。
  • rebase:原方法会被重命名并保留原逻辑,所以能够在通过@SuperCall Callable<?> superCall保留执行原方法逻辑执行的情况下,继续执行我们自定义的修改/增强逻辑

使用rebase生成了两个class,一个为BuddyUserManager.class,一个为辅助类BuddyUserManager$auxiliary$5FSta4Vk

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

2.11 rebase, redefine默认生成类名

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

  • .subclass(目标类.class)
    • 超类为jdk自带类: net.bytebuddy.renamed.{超类名}$ByteBuddy${随机字符串}
    • 超类非jdk自带类 {超类名}$ByteBuddy${随机字符串}
  • .rebase(目标类.class):和目标类的类名一致(效果上即覆盖原本的目标类class文件)
  • .redefine(目标类.class):和目标类的类名一致(效果上即覆盖原本的目标类class文件)

这里就不写示例代码了,实验的方式很简单,即把自己指定的类名.name(yyy.zzz.Xxxx)去掉,即根据默认命名策略生成类名

2.12 bytebuddy的类加载器

2.12.1 注意点

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

    Byte Buddy默认使用WRAPPER类加载策略,该策略会优先根据类加载的双亲委派机制委派父类加载器加载指定类,若类成功被父类加载器加载,此处仍通过.load加载类就报错。(直观上就是将生成的类的.class文件保存到本地后,继续执行.load方法会抛异常java.lang.IllegalStateException: Class already loaded

  • 若使用CHILD_FIRST类加载策略,那么打破双亲委派机制,优先在当前类加载器加载类(直观上就是将生成的类的.class文件保存到本地后,继续执行.load方法不会报错,.class类由ByteBuddy的ByteArrayClassLoader正常加载)。具体代码可见net.bytebuddy.dynamic.loading.ByteArrayClassLoader.ChildFirst#loadClass

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

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

其他关于类加载的介绍,可以查阅Byte Buddy官方教程文档的"类加载"章节,下面内容摘自官方教程文档

目前为止,我们只是创建了一个动态类型,但是我们并没有使用它。Byte Buddy 创建的类型是通过DynamicType.Unloaded的一个实例来表示的。通过名称可以猜到,这些类不会加载到JVM。 相反,Byte Buddy 创建的类以Java 类文件格式的二进制结构表示。 这样的话,你可以决定用生成的类来做什么。例如,你或许想从构建脚本运行 Byte Buddy,该脚本仅在部署前生成类以增强 Java 应用。 对于这个目的,DynamicType.Unloaded类允许提取动态类型的字节数组。为了方便, 该类型还额外提供了saveIn(File)方法,该方法允许你将一个类保存到给定的文件夹。此外, 它允许你通过inject(File)方法将类注入到已存在的 jar 文件。

虽然直接访问一个类的二进制结构是直截了当的,但不幸的是加载一个类更复杂。在 Java 里,所有的类都用ClassLoader(类加载器)加载。 这种类加载器的一个示例是启动类加载器,它负责加载 Java 类库里的类。另一方面,系统类加载器负责加载 Java 应用程序类路径里的类。 显然,这些预先存在的类加载器都不知道我们创建的任何动态类。为了解决这个问题,我们需要找其他的可能性用于加载运行时生成的类。 Byte Buddy 通过开箱即用的不同方法提供解决方案:

  • 我们仅仅创建一个新的ClassLoader,它被明确地告知存在一个特定的动态创建的类。 因为 Java 类加载器是按层级组织的,我们定义的这个类加载器是程序里已经存在的类加载器的孩子。这样, 程序里的所有类对于新类加载器加载的动态类型都是可见的。
  • 通常,Java 类加载器在尝试直接加载给定名称的类之前会询问他的父类加载器。这意味着,在父类加载器知道有相同名称的类时, 子类加载器通常不会加载类。为此,Byte Buddy 提供了孩子优先创建的类加载器,它在询问父类加载器之前会尝试自己加载类。 除此之外,这种方法类似于刚才上面提及的方法。注意,这种方法不会覆盖父类加载器加载的类,而是隐藏其他类型。
  • 最后,我们可以用反射将一个类注入到已存在的类加载器。通常,类加载器会被要求通过类名称来提供一个给定的类。 用反射我们可以扭转这个规则,调用受保护的方法将一个新类注入类加载器,而类加载器实际上不知道如何定位这个动态类。

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

  • 如果我们创建一个新的ClassLoader,这个类加载器会定义一个新的命名空间。 这样可能会通过两个不同的类加载器加载两个有相同名称的类。这两个类永远不会被JVM视为相等,即时这两个类是相同的类实现。 这个相等规则也适用于 Java 包。这意味着,如果不是用相同的类加载器加载, example.Foo类无法访问example.Bar类的包私有方法。此外, 如果example.Bar继承example.Foo,任何被覆写的包私有方法都将变为无效,但会委托给原始实现。
  • 每当加载一个类时,一旦引用另一种类型的代码段被解析,它的类加载器将查找该类中引用的所有类型。该查找会委托给同一个类加载器。 想象一下这种场景:我们动态的创建了example.Fooexample.Bar两个类, 如果我们将example.Foo注入一个已经存在的类加载器,这个类加载器可能会尝试定位查找example.Bar。 然而,这个查找会失败,因为后一个类是动态创建的,而且对于刚才注入example.Foo类的类加载器来说是不可达的。 因此反射的方法不能用于在类加载期间生效的带有循环依赖的类。幸运的是,大多数JVM的实现在第一次使用时都会延迟解析引用类, 这就是类注入通常在没有这些限制的时候正常工作的原因。此外,实际上,由 Byte Buddy 创建的类通常不会受这样的循环影响

你可能会任务遇到循环依赖的机会是无关紧要的,因为一次只创建一个动态类。然而,动态类型的创建可能会触发辅助类型的创建。 这些类型由 Byte Buddy 自动创建,以提供对正在创建的动态类型的访问。我们将在下面的章节学习辅助类型,现在不要担心这些。 但是,正因为如此,我们推荐你尽可能通过创建一个特定的ClassLoader来加载动态类, 而不是将他们注入到一个已存在的类加载器。

创建一个DynamicType.Unloaded后,这个类型可以用ClassLoadingStrategy加载。 如果没有提供这个策略,Byte Buddy 会基于提供的类加载器推测出一种策略,并且仅为启动类加载器创建一个新的类加载器, 该类加载器不能用反射的方式注入任何类。否则为默认设置。

Byte Buddy 提供了几种开箱即用的类加载策略, 每一种都遵循上述概念中的其中一个。这些策略都在ClassLoadingStrategy.Default中定义,其中, WRAPPER策略会创建一个新的,经过包装的ClassLoaderCHILD_FIRST策略会创建一个类似的具有孩子优先语义的类加载器,INJECTION策略会用反射注入一个动态类型

WRAPPERCHILD_FIRST策略也可以在所谓的*manifest(清单)*版本中使用,即使在类加载后, 也会保留类的二进制格式。这些可替代的版本使类加载器加载的类的二进制表示可以通过ClassLoader::getResourceAsStream方法访问。 但是,请注意,这需要这些类加载器保留一个类的完整的二进制表示的引用,这会占用 JVM 堆上的空间。因此, 如果你打算实际访问类的二进制格式,你应该只使用清单版本。由于INJECTION策略通过反射实现, 而且不可能改变方法ClassLoader::getResourceAsStream的语义,因此它自然在清单版本中不可用。

让我们看一下这样的类加载:

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

在上面的示例中,我们创建并加载了一个类。像我们之前提到的,我们用WRAPPER加载策略加载类, 它适用于大多数场景。最后,getLoaded方法返回了一个现在已经加载的 Java Class(类)的实例, 这个实例代表着动态类。

注意,当加载类时,预定义的类加载策略是通过应用执行上下文的ProtectionDomain来执行的。或者, 所有默认的策略通过调用withProtectionDomain方法来提供明确地保护域规范。 当使用安全管理器或使用签名jar包中定义的类时,定义一个明确地保护域是非常重要的。

2.13 自定义类的加载路径

2.13.1 注意点

  • ClassFileLocator:类定位器,用来定位类文件所在的路径,支持jar包所在路径,.class文件所在路径,类加载器等。
    • ClassFileLocator.ForJarFile.of(File file):jar包所在路径
    • ClassFileLocator.ForFolder(File file).class文件所在路径
    • ClassFileLocator.ForClassLoader.ofXxxLoader():类加载器
    • 一般使用时都需要带上ClassFileLocator.ForClassLoader.ofSystemLoader(),才能保证jdk自带类能够正常被扫描识别到,否则会抛出异常(net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description for java.lang.Object)。
  • ClassFileLocator.Compound:本身也是类定位器,用于整合多个ClassFileLocator
  • TypePool:类型池,一般配合ClassFileLocator.Compound使用,用于从指定的多个类定位器内获取类描述对象
    • 调用typePool.describe("全限制类名").resolve()获取TypeDescription类描述对象,resolve()不会触发类加载。
  • TypeDescription:类描述对象,用于描述java类,后续subclass, rebase, redefine时用于指定需要修改/增改的类。

其他介绍可见官方教程文档的"重新加载类"和"使用未加载的类"章节,下面内容摘至官方教程文档:

使用 Java 的 HotSwap 功能有一个巨大的缺陷,HotSwap的当前实现要求重定义的类在重定义前后应用相同的类模式。 这意味着当重新加载类时,不允许添加方法或字段。我们已经讨论过 Byte Buddy 为任何变基的类定义了原始方法的副本, 因此类的变基不适用于ClassReloadingStrategy。此外,类重定义不适用于具有显式的类初始化程序的方法(类中的静态块)的类, 因为该初始化程序也需要复制到额外的方法中。不幸的是, OpenJDK已经退出了扩展HotSwap的功能, 因此,无法使用HotSwap的功能解决此限制。同时,Byte Buddy 的HotSwap支持可用于某些看起来有用的极端情况。 否则,当(例如,从构建脚本)增强存在的类时,变基和重定义可能是一个便利的功能。

意识到HotSwap功能的局限性后,人们可能会认为变基重定义指令的唯一有意义的应用是在构建期间。 通过应用构建时的处理,人们可以断言一个已经处理过的类在它的初始类简单地加载之前没有被加载,因为这个类加载是在不同的JVM实例中完成的。 然而,Byte Buddy 同样有能力处理尚未加载的类。为此,Byte Buddy 抽象了 Java 的反射 API,例如, 一个Class实例在内部由一个TypeDescription表示。事实上, Byte Buddy 只知道如何处理由实现了TypeDescription接口的适配器提供的Class。 这种抽象的最大好处是类的信息不需要由类加载器提供,而是可以由其他的源提供。

**Byte Buddy 使用TypePool(类型池),提供了一种标准的方式来获取类的TypeDescription(类描述)**。当然, 这个池的默认实现也提供了。TypePool.Default的实现解析类的二进制格式并将其表示为需要的TypeDescription。 类似于类加载器为加载好的类维护一个缓存,该缓存也是可定制的。此外,它通常从类加载器中检索类的二进制格式, 但不指示它加载此类

示例代码:

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

/**
 * 自定义类的加载路径
 */
@Test
public void testCustomClassLoader() throws IOException, InstantiationException, IllegalAccessException {
    // 从指定jar包加载,可能是外部包
    ClassFileLocator beansJarFileLocator = ClassFileLocator
        .ForJarFile
        .of(new File("D:\\apache-maven-3.6.3\\repos\\org\\springframework\\spring-beans\\5.2.12.RELEASE\\spring-beans-5.2.12.RELEASE.jar"));

    ClassFileLocator coreJarFileLocator = ClassFileLocator
        .ForJarFile
        .of(new File("D:\\apache-maven-3.6.3\\repos\\org\\springframework\\spring-core\\5.2.12.RELEASE\\spring-core-5.2.12.RELEASE.jar"));

    // 从指定目录加载 .class 文件
    ClassFileLocator.ForFolder jarFolder = new ClassFileLocator
        .ForFolder(new File("D:\\idea_workspace\\bank-credit-sy\\credit-support\\credit-uaa\\uaa-server\\target\\classes"));

    // 系统类加载器,如果不加会找不到jdk本身的类
    ClassFileLocator systemLoader = ClassFileLocator.ForClassLoader.ofSystemLoader();

    // 创建一个组合类加载器
    ClassFileLocator.Compound compound = new ClassFileLocator.Compound(beansJarFileLocator, systemLoader, coreJarFileLocator, jarFolder);
    TypePool typePool = TypePool.Default.of(compound);

    // 写入全类名称,获取对应对象,并不会触发类的加载
    TypeDescription typeDescription = typePool.describe("org.springframework.beans.factory.support.RootBeanDefinition").resolve();

    // unloaded表示字节码还未加载到jvm中
    DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
        // 变基
        .redefine(typeDescription, compound)
        .name("cn.git.budy.test.BuddyUserManager")
        // 通过名称sizeOf拦截静态方法
        .method(named("getTargetType"))
        .intercept(FixedValue.nullValue())
        .make();

    unloaded.saveIn(new File(path));

    // 加载文件夹中的类
    TypeDescription typeDescriptionClassFolder = typePool.describe("cn.git.auth.dto.HomeDTO").resolve();
    DynamicType.Unloaded<Object> classFolderUnLoaded = new ByteBuddy()
        // 变基
        .redefine(typeDescriptionClassFolder, compound)
        .name("cn.git.budy.test.BuddyUserManagerClassFolder")
        // 通过名称sizeOf拦截静态方法
        .method(named("getCurrentLoginUserCd"))
        .intercept(FixedValue.nullValue())
        .make();
    classFolderUnLoaded.saveIn(new File(path));
}

最终生成代码效果如下:

在这里插入图片描述
在这里插入图片描述

2.14 清空方法体

2.14.1 注意点

  • ElementMatchers.isDeclaredBy(Class<?> type)):拦截仅由目标类声明的方法,通常用于排除超类方法
  • StubMethod.INSTANCE:Byte Buddy默认的拦截器方法实现之一,会根据被拦截的目标方法的返回值类型返回对应的默认值
    1. The value 0 for all numeric type.
    2. The null character for the char type.
    3. false for the boolean type.
    4. Nothing for void types.
    5. A null reference for any reference types. Note that this includes primitive wrapper types.
  • 当使用ElementMatchers.any()时,仅subclass包含构造方法,rebaseredefine不包含构造方法
  • 使用ElementMatchers.any().and(ElementMatchers.isDeclaredBy(目标类.class))时,仅subclass支持修改生成类名,rebaseredefine若修改类名则拦截后的修改/增强逻辑无效。

演示代码:

/**
 * 清空方法体,起到保护源码的作用
 */
@Test
public void testEmptyMethodBody() throws IOException, InstantiationException, IllegalAccessException {
    // unloaded表示字节码还未加载到jvm中
    DynamicType.Unloaded<UserManager> unloaded = new ByteBuddy()
        // 指定父类
        .redefine(UserManager.class)
        // .name("cn.git.budy.test.BuddyUserManager")
        // named通过名字指定要拦截的方法,还可以使用返回类型进行匹配
        // .and(isDeclaredBy(UserManager.class)) 父类方法重写清空 equals,toString,hashCode
        .method(any())
        // 预制拦截器清空方法
        .intercept(StubMethod.INSTANCE)
        .make();

    // 保存到本地
    unloaded.saveIn(new File(path));
}

三、java agent

3.1 原生jdk实现

3.1.1 注意点

  • premain方法在main之前执行
  • Instrumentation#addTransformer(ClassFileTransformer transformer):注册字节码转换器,这里在premain方法内注册,保证在main方法执行前就完成字节码转换
  • 字节码中类名以/间隔,而不是.间隔

关于java agent,网上也有很多相关文章,这里不多做介绍,这里简单链接一些文章:

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

Java探针(javaagent) - 简书 (jianshu.com)

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

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

3.1.2 示例代码

新建一个module为agent-jdk,这里图方便,里面主要实现了premain方法,以及一个简单的例子,对一个自定义类TestService类的加强,引入pom信息如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.git</groupId>
        <artifactId>bytebuddy-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>agent-jdk</artifactId>
    <packaging>jar</packaging>

    <name>agent-jdk</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <!-- 用于打包插件 -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <!-- MANIFEST.MF 配置项,指定premain方法所在类 -->
                            <Premain-Class>cn.git.AgentDemo</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- 什么阶段会触发此插件 -->
                        <phase>package</phase>
                        <goals>
                            <!-- 只运行一次 -->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

探针的premain接口实现:

package cn.git;

import lombok.extern.slf4j.Slf4j;

import java.lang.instrument.Instrumentation;

/**
 * @description: 探针启动入口
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-19
 */
public class AgentDemo {

    /**
     * premain方法,main方法执行之前进行调用,插桩代码入口
     * @param args 标识外部传递参数
     * @param instrumentation 插桩对象
     */
    public static void premain(String args, Instrumentation instrumentation) {
        System.out.println("进入到premain方法,参数args[" + args + "]");
        instrumentation.addTransformer(new ClassFileTransformerDemo());
    }
}

本地实现简单的TestService类增强:

package cn.git;

import javassist.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/**
 * @description: 类文件转换器
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-19
 */
public class ClassFileTransformerDemo implements ClassFileTransformer {
    /**
     * 当字节码第一次被加载时,会调用该方法
     * @param className 加载的类的全限定名,包含包名,例如:cn/git/service/TestService/test
     *
     * @return 需要增强就返回增强后的字节码,否则返回null
     */
    @Override
    public 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 {

    @Autowired
    private 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}.
     */
    @Override
    public 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;
        }

        // 实例化 SpringMvcInterceptor
        SpringMvcInterceptor 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.
     */
    @Override
    public 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.
     */
    @Override
    public 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.
     */
    @Override
    public 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.
     */
    @Override
    public 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.
     */
    @Override
    public 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>() {
            @Override
            public 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}.
     */
    @Override
    public 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
     */
    @RuntimeType
    public 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>() {
            @Override
            public 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}.
     */
    @Override
    public 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
     */
    @RuntimeType
    public void intercept(
            @This Object targetObject,
            @AllArguments Object[] targetMethodArgs) {
       System.out.println("增强构造方法参数intercept: " + Arrays.toString(targetMethodArgs));
    }
}

之后我们同样打包,放置到相同文件夹中,启动server服务,并且观察构造方法已经被增强,执行了增强逻辑:

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263337.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2025erp系统开源免费进销存系统搭建教程/功能介绍/上线即可运营软件平台源码

系统介绍 基于ThinkPHP与LayUI构建的全方位进销存解决方案 本系统集成了采购、销售、零售、多仓库管理、财务管理等核心功能模块&#xff0c;旨在为企业提供一站式进销存管理体验。借助详尽的报表分析和灵活的设置选项&#xff0c;企业可实现精细化管理&#xff0c;提升运营效…

python 高级用法

1、推导列表 ans [ x for x in range(6)] print(ans)ans [ x for x in range(6) if x > 2] print(ans)ans [ x*y for x in range(6) if x > 2 for y in range(6) if y < 3] print(ans) 2、map 函数 a list(map(list,"abc")) print(a) b list(map(ch…

flask_socketio 以继承 Namespace方式实现一个网页聊天应用

点击进入上一篇&#xff0c;可作为参考 实验环境 python 用的是3.11.11 其他环境可以通过这种方式一键安装&#xff1a; pip install flask3.1.0 Flask-SocketIO5.4.1 gevent-websocket0.10.1 -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple pip list 详情如下&am…

Redis 7.x如何安装与配置?保姆级教程

大家好&#xff0c;我是袁庭新。最新写了一套最新版的Redis 7.x企业级开发教程&#xff0c;今天先给大家介绍下Redis 7.x如何在Linux系统上安装和配置。 1 Redis下载与安装 使用非关系型数据库Redis必须先进行安装配置并开启Redis服务&#xff0c;然后使用对应客户端连接使用…

如何编辑调试gradle,打印日志

在build.gradle.kts中输入 println("testxwg1 ") logger.lifecycle("testxwg2") logger.log(LogLevel.ERROR,"testxwg5") 点刷新就能看到打印日志了

electron-vite【实战系列教程】

创建项目 https://blog.csdn.net/weixin_41192489/article/details/144442262 安装必要的插件 UI 库 element-plus npm install element-plus --save安装 element-plus 图标 npm install element-plus/icons-vue安装插件 – 自动注册组件 vs 自动导入框架方法 npm install -…

【开源项目】数字孪生轨道~经典开源项目数字孪生智慧轨道——开源工程及源码

飞渡科技数字孪生轨道可视化平台&#xff0c;基于国产数字孪生引擎&#xff0c;结合物联网IOT、大数据、激光雷达等技术&#xff0c;对交通轨道进行超远距、高精度、全天侯的监测&#xff0c;集成轨道交通运营数据&#xff0c;快速准确感知目标&#xff0c;筑牢轨交运营生命线。…

Rstudio安装

Rstudio提供了良好的R语言代码编辑环境&#xff0c;R程序调试环境&#xff0c;图形可视化环境以及方便的R工作空间和工作目录管理。 下载网址&#xff1a;https://posit.co/products/open-source/rstudio/ 进入网址&#xff1a; 下滑找到&#xff0c;点击进入 找到Dsektop&am…

Chrome 浏览器原生功能截长屏

我偶尔需要截取一些网页内容作为素材&#xff0c;但偶尔内容很长无法截全&#xff0c;需要多次截屏再拼接&#xff0c;过于麻烦。所以记录下这个通过浏览器原生功能截长屏的方案。 注意 这种方案并不是百分百完美&#xff0c;如果涉及到一些需要滚动加载的数据或者悬浮区块&am…

【工具】通过js获取chrome浏览器扩展程序列表id及名称等

【工具】通过js获取chrome浏览器扩展程序列表id及名称等 第一步 打开扩展程序页面 chrome://extensions/ 第二部 注入js获取 let 扩展字典 {} document.querySelector("body > extensions-manager").shadowRoot.querySelector("#items-list").shadow…

基于LSB最低有效位的音频水印嵌入提取算法FPGA实现,包含testbench和MATLAB对比

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 vivado2019.2 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视…

Midjourney参数大全

基本参数​ 纵横比&#xff0c;宽高比​ --aspect&#xff0c;或--ar更改生成的纵横比。 混乱​ --chaos <number 0–100>改变结果的变化程度。更高的数值会产生更多不寻常和意想不到的结果。 图像权重​ --iw <0–2>设置相对于原始图像相识度。默认值为 1&a…

虚拟机VMware的安装问题ip错误,虚拟网卡

要么没有虚拟网卡、有网卡远程连不上等 一般出现在win11 家庭版 1、是否IP错误 ip addr 2、 重置虚拟网卡 3、查看是否有虚拟网卡 4、如果以上检查都解决不了问题 如果你之前有vmware 后来卸载了&#xff0c;又重新安装&#xff0c;一般都会有问题 卸载重装vmware: 第一…

Loki 微服务模式组件介绍

目录 一、简介 二、架构图 三、组件介绍 Distributor&#xff08;分发器&#xff09; Ingester&#xff08;存储器&#xff09; Querier&#xff08;查询器&#xff09; Query Frontend&#xff08;查询前端&#xff09; Index Gateway&#xff08;索引网关&#xff09…

EMQX V5 使用API 密钥将客户端踢下线

在我们选用开源的EMQX作为mqtt broker&#xff0c;我们可能会考虑先让客户端连接mqtt broker成功&#xff0c;再去校验客户端的有效性&#xff0c;当该客户端认证失败&#xff0c;再将其踢下线。例如&#xff1a;物联网设备连接云平台时&#xff0c;我们会将PK、PS提前烧录到设…

Python中所有子图标签Legend显示详解

在数据可视化中&#xff0c;图例&#xff08;legend&#xff09;是一个非常重要的元素&#xff0c;它能够帮助读者理解图表中不同元素的含义。特别是在使用Python进行可视化时&#xff0c;matplotlib库是一个非常强大的工具&#xff0c;能够轻松创建包含多个子图的图表&#xf…

TCP套接字通信与守护进程

目录 TCP socket API 详解 代码实现TCP通讯 服务端 客户端 Task 守护进程 守护进程 前台与后台 Linux进程间关系 ​编辑 设置为独立会话setsid daemon接口 为什么需要设置umask 会话ID与组ID TCP的相关机制 下图是基于TCP协议的客户端/服务器程序的一般流程: 数…

单点登录平台Casdoor搭建与使用,集成gitlab同步创建删除账号

一&#xff0c;简介 一般来说&#xff0c;公司有很多系统使用&#xff0c;为了实现统一的用户名管理和登录所有系统&#xff08;如 GitLab、Harbor 等&#xff09;&#xff0c;并在员工离职时只需删除一个主账号即可实现权限清除&#xff0c;可以采用 单点登录 (SSO) 和 集中式…

OCR:文字识别

使用场景: 远程身份认证 自动识别录入用户身份/企业资质信息&#xff0c;应用于金融、政务、保险、电商、直播等场景&#xff0c;对用户、商家、主播进行实名身份认证&#xff0c;有效降低用户输入成本&#xff0c;控制业务风险 文档电子化 识别提取各类办公文档、合同文件、企…

亚信安全春节14天双倍假期通告

亚信安全14天双倍假期来袭 “网安福利王”再次实至名归 2024年 8773小时&#xff0c;31582680秒 亚信安全一直驰骋于云网安世界 奋战在“安全 数智化”的壮阔征途上 如今&#xff0c;新春的脚步渐近 长达14天的春节长假 能让我们暂且放下忙碌的工作 去除班味&#xff0c…