参考文档:ByteBuddy(史上最全)

1. 字节码增强简介

Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个 class文件 具有如下固定的数据格式,

ClassFile {
    u4             magic;           // 魔数,固定为0xCAFEBABE
    u2             minor_version;   // 次版本
    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
    u2             constant_pool_count;                     // 常量池个数
    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义
    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
    u2             this_class;      // 类索引
    u2             super_class;     // 父类索引
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

可以看到,class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。

class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。

了解Java字节码有助于字节码增强的开发,但并不是实现字节码增强开发的必要条件,最新主流的众多字节码增强工具框架类库都将字节码的编排进行了不同程度封装,在可读性、易编排性、排错性上提供开发便利性,学习曲线和开发难度得到了较好的改善。

2. 字节码增强支持

其实,热部署和 arthas 用的都是字节码增强 。

对于字节码增强的开发来说,JVMTI是一个在实践中应该被熟悉的工具技术。

JVM从1.5版本开始提供 JVM Tool Interface ,这是JVM对外的、用于Java应用监控和调试的一系列工具接口,是JVM平台调试架构的重要组成部分。

下图是 JVM平台调试架构图

The Java™ Platform Debugger Architecture is structured as follows:
           Components                          Debugger Interfaces

                /    |--------------|
               /     |     VM       |
 debuggee ----(      |--------------|  <------- JVM TI - Java VM Tool Interface(Jvm服务端调试接口)
               \     |   back-end   |
                \    |--------------|
                /           |
 comm channel -(            |  <--------------- JDWP - Java Debug Wire Protocol (Java调试通信协议)
                \           |
                     |--------------|
                     | front-end    |
                     |--------------|  <------- JDI - Java Debug Interface (客户端调试接口和调试应用)
                     |      UI      |
                     |--------------|

2.1 加载时机介绍

JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。

我们可以看下Java agent代理的两个入口函数定义,

// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);

// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);

这两个入口函数定义分别对应于JVM TI专门提供了执行 字节码增强(bytecode instrumentation) 的两个接口。

  • 加载时刻增强(JVM 启动时加载),类字节码文件在JVM加载的时候进行增强。

  • 动态增强(JVM 运行时加载),已经被JVM加载的class字节码文件,当被修改或更新时进行增强,从JDK 1.6开始支持。

这两个接口都是从JDK 1.6开始支持。

我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和 Java Instrument类包 封装好了字节码增强的上述接口通信。

上面我们已经说到了, 有两处地方可以进行 Java Agent 的加载,分别是 目标JVM启动时加载 和 目标JVM运行时加载,这两种不同的加载模式使用不同的入口函数:

2.2 启动时加载

入口函数如下所示:

 // 函数1
public static void premain(String agentArgs, Instrumentation inst);
// 函数2
public static void premain(String agentArgs);

JVM 首先寻找函数1,如果没有发现函数1,则会寻找函数2

演示类

package com.rs.demo;
import java.lang.instrument.Instrumentation;

public class AgentDemo {
    /**
     * JVM 尝试在代理类上调用以下方法
     * 该方法在main方法之前运行,
     * 与main方法运行在同一个JVM中
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========premain方法执行 1========");
        System.out.println("agentArgs:="+agentArgs);

    }
}

声明

我们需要创建resources/META-INF.MANIFEST.MF 文件,当 jar包打包时将文件一并打包,文件内容如下:

Manifest-Version: 1.0
Can-Redefine-Classes: true   # true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true    # true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class:  com.rs.demo.AgentDemo   #premain方法所在类的位置

使用

我们直接在 JVM 启动参数中加入 -javaagent 参数并指定 jar 文件的位置

指定agent程序并运行该类
java -javaagent:/home/javassist-demo.jar TargetJvm

执行结果

=========premain方法执行 1========
agentArgs:=null

自己程序结果

2.3 Attach Api

要实现动态调试,我们就不能将目标 JVM 停机后再重新启动,这不符合我们的初衷,因此我们可以使用 JDK 的 Attach Api 来实现运行时挂载 Agent。

Attach Api 是 SUN 公司提供的一套扩展 API,用来向目标 JVM 附着(attach)在目标程序上,有了它我们可以很方便地监控一个 JVM。

Attach Api 对应的代码位于 com.sun.tools.attach包下,提供的功能也非常简单:

  • 列出当前所有的 JVM 实例描述
  • Attach 到其中一个 JVM 上,建立通信管道
  • 让目标JVM加载Agent

该包下有一个类 VirtualMachine,它提供了两个重要的方法:

// 传递一个进程号,返回目标 JVM 进程的 vm 对象,该方法是 JVM进程之间指令传递的桥梁,底层是通过 socket 进行通信
VirtualMachine attach(String var0)
// 该方法允许我们将 agent 对应的 jar 文件地址作为参数传递给目标 JVM,目标 JVM 收到该命令后会加载这个 Agent
void loadAgent(String var1)

有了 Attach Api ,我们就可以创建一个java进程,用它attach到对应的jvm,并加载agent。

2.4 运行时加载

入口函数如下所示:

// 函数1
public static void agentmain(String agentArgs, Instrumentation inst);
// 函数2
public static void agentmain(String agentArgs);

与上述一致,JVM 首先寻找函数1,如果没有发现函数1,则会寻找函数2

这两组方法的第一个参数 agentArgs 是随同 -javaagent 一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这参数,inst 是 Instrumentation 类型的对象,是 JVM 自己传入的,我们可以拿这个参数进行参数的增强操作。

写完代理后,打成 jar 包。

通过 attach api 把 agent 加载进正在运行的 jvm。

public class Test {
    public static void main(String[] args) throws Exception {
        VirtualMachine virtualMachine = VirtualMachine.attach("要 attach jvm 的 pid");
        virtualMachine.loadAgent("E:\\Programming\\java-agent\\target\\java-agent-1.0.jar", null);
        virtualMachine.detach();
    }
}

3. 增强类库

3.1 Javassist

Javassist 是一个非常早的字节码操作类库,开始于1999年,

它能够支持两种编辑方式:

  • 源码级别
  • 字节码指令级别

相比于晦涩的字节码级别,源码级别更加人性化,代码编写起来更加易懂。

以上面的ASM字节码指令编辑为例,换成对应的Javassist源码级别编辑方式,如下所示,

CtMethod m = cc.getDeclaredMethod("sayHello");
m.insertBefore("{ System.out.println(\"begin of sayhello()\"); }");

相信大多数程序员更愿意接受源码级别编辑方式,翻译成直接码指令的工作就交给Javassist完成,目前源码级别方式Javassist只支持Java语言语法。

3.2 ASM

ASM 是一个Java字节码解析和操作框架,整个类包非常小,还不到120KB,但其非常注重对类字节码的操作速度,

这种高性能来自于它的设计模式 - 访问者模式,即通过Reader、Visitor和Writer模式。

ASM是直接操作类字节码数据,因此其读写的是字节码指令,比如,

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("begin of sayhello().");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

这种指令比较晦涩难懂,在实际操作过程中,会先将期望的类源码文件写好,编译后查看字节码文件,然后复制相关字节码指令。

3.3 增强类库关系图

img

需要提一下,JDK Proxy和Cglib也是以代码方式进行类方法的切面增强,但它们都是以框架的方式实现了Java类的动态扩展,主要应用在框架级别的字节码增强,在某种程度上JDK Proxy和Cglib技术对应用是有代码侵入的,这里的侵入不仅仅是框架代码侵入,而且包括增强的类中依赖JDK Proxy和Cglib类。

与此相比,ButeBuddy API是以无侵入方式加强类代码,设计理念更优。

3.4 工具对比

对比 ASM Javassist JDK Proxy Cglib ByteBuddy
起源时间 2002 1999 2000 2011 2014
包大小 130KB (版本9.3) 788KB (版本3.28.0-GA) 3.7MB (版本1.10.19)
增强方式 字节码指令 字节码指令和源码(注:源码文本) 源码 源码 源码
源码编译 NA 不支持 支持 支持 支持
agent支持 支持 支持 不支持,依赖框架 不支持,依赖框架 支持
性能
维护状态 停止升级 停止维护 活跃
优点 超高性能,应用场景广泛 同时支持字节码指令和源码两种增强方式 JDK原生类库支持 零侵入,提供良好的API扩展编程
缺点 字节码指令对应用开发者不友好 场景非常局限,只适用于Java接口 已经不再维护,对于新版JDK17+支持不好,官网建议切换到ByteBuddy
应用场景 小,高性能,广泛用于语言级别 广泛用于框架场景 广泛用于Trace场景

4. Byte Buddy 简介

Byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。

除了Java类库附带的代码生成实用程序外,Byte Buddy还允许创建任意类,并且不限于实现用于创建运行时代理的接口。

此外,Byte Buddy提供了一种方便的API,可以使用Java代理或在构建过程中手动更改类。

无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。

已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。

就像它的官网介绍;

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。

此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。
已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
2015年10月,Byte Buddy被 Oracle 授予了 Duke’s Choice大奖。

4.1 快速入门

依赖

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.10.19</version>
</dependency>

案例

String helloWorld = new ByteBuddy()
            .subclass(Object.class)
            .method(named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make()
            .load(getClass().getClassLoader())
            .getLoaded()
            .newInstance()
            .toString();    

System.out.println(helloWorld);  // Hello World!

他的运行结果就是一行,Hello World!,

整个代码块核心功能:

  1. 通过 method(named(“toString”)),找到 toString 方法。
  2. 再通过拦截 intercept,设定此方法的返回值。FixedValue.value(“Hello World!”)

到这里其实一个基本的方法就通过 Byte-buddy ,改造完成。

  1. 接下来的这一段主要是用于加载生成后的 Class
  2. newInstance().toString(); 的作用 是: 执行以及调用方法 toString()

方法介绍

  • subclass(Object.class) :创建一个Object的子类
  • name(“ExampleClass”) : 新建的类名叫做“ExampleClass” ,暂时没有用到
  • method() :要拦截“ExampleClass”中的方法
  • ElementMatchers.named(“toString”) :拦截条件,拦截toString()这个方法, 没有条件,表示所有的方法
  • intercept() :指定了拦截到的方法要修改成什么样子,是不是和 Spring AOP有点像了
  • make() :创建上面生成的这个类型
  • load() :加载这个生成的类
  • newInstance() :Java 反射的API,创建实例

4.2 输出 class

我们通过字节码输出到文件,看下具体被改造后的样子

在Byte buddy中默认提供了一个 dynamicType.saveIn() 方法,可以保存编译后的Class文件

public static void main(String[] args) {
    Unloaded<Object> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .method(named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make();
    Loaded<Object> type = dynamicType.load(getClassLoader());
    
    // 输出类信息到目标文件夹下
    dynamicType.saveIn(new File("target/classes/com/rs"));
    
}

输出的class 文件,反编译过来的 java 文件,idea打开 如下:

class

4.3 创建类和方法

4.3.1 创建类的基础结构

  • subclass 设置基类
  • name 设置类的名称, 如果不写类名会自动生成要给类名。
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class)
        .name("com.crazymaker.circle.bytecode.enhancement.HelloWorld")
        .make();

通过增强之后,得到 DynamicType.Unloaded 对象

4.3.2 三种动态增强的方式

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(Object.class) // 生成 Object的子类
        .name("com.fatsnake.Type")   // 生成类的名称为"com.xxx.Type"
        .make();
  • subclass:
    对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。

  • rebasing:
    对应 ByteBuddy.rebasing() 方法。

    当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。

    从而达到不丢失实现的目的。

    这些重命名的方法可以继续通过重命名后的名称进行调用。

    例如:

    class Foo { // Foo的原始定义
    
      String bar() { return "bar"; }
    }
    
    class Foo { // 增强后的Foo定义
      String bar() { return "foo" + bar$original(); }
    // 目标类原有方法
      private String bar$original() { return "bar"; }
    }
    
  • redefinition:

    对应 ByteBuddy.redefine() 方法。

    当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,删除已经存在的方法实现。

    如果使用其他的方法实现, 去替换已经存在的方法实现,则原来存在的方法实现就会消失。

    例如,这里依然是增强 Foo 类的 bar() 方法使其直接返回 “unknow” 字符串,增强结果如下:

    class Foo { // 增强后的Foo定义
      String bar() { return "unknow"; }
    }
    

4.3.3 类加载策略

DynamicType.Unloaded 对象,表示的是一个未加载的类型,通过在 ClassLoadingStrategy.Default中定义的加载策略,加载此类型。

Class<?> loaded = new ByteBuddy()
        .subclass(Object.class)
        .name("com.xxx.Type")
        .make()
        // 使用 WRAPPER 策略加载生成的动态类型
        .load(Main2.class.getClassLoader(), 
              ClassLoadingStrategy.Default.WRAPPER)
        .getLoaded();
  • WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  • CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
  • INJECTION 策略:使用反射, 将动态生成的类型直接注入到当前 ClassLoader 中。

4.3.4 创建方法

  • defineMethod 定义方法
  • withParameter 设置参数
  • intercept 拦截设置返回值

创建main方法的代码如下:

在这里插入图片描述

与上面相比新增的代码片段;

  • defineMethod(“main”, String.class, Modifier.PUBLIC + Modifier.STATIC)
    定义方法;名称、返回类型、属性 public static void Modifier.PUBLIC + Modifier.STATIC,这是一个是二进制相加,每一个类型都在二进制中占有一位。例如 1 2 4 8 … 对应的二进制占位 1111。既可以执行相加运算,并又能保留原有单元的属性。

  • withParameter(String[].class, “args”),定义参数;参数类型、参数名称

  • intercept(FixedValue.value(“Hello World!”)),拦截设置返回值,但此时还能满足我们的要求。

输出的class 文件,反编译过来的 java 文件,idea打开 如下:

在这里插入图片描述

此时基本已经可以看到我们平常编写的 Hello World 影子了,但还能输出结果。

注意,如果返回值为void,那么 intercept设置的,变成了 一个局部变量了

4.3.5 创建字段

defineField() 方法:创建字段。

4.3.6 实现接口

implement() 方法:实现接口。

下面是一个例子

在这里插入图片描述

输出的class 文件,反编译过来的 java 文件,idea打开 如下:

在这里插入图片描述

4.4 委托函数调用(重点)

这是重点:

为了能让我们使用字节码编程创建的方法,去调用另外一个同名方法,那么这里需要使用到委托。

4.4.1 委托函数调用实例

通过 MethodDelegation 去完成

  • 在 intercept 方法中,使用 MethodDelegation.to 委托到静态方法
    intercept(MethodDelegation.to(DelegateClazz.class)) // 委托到 DelegateClazz 的静态方法

  • 在intercept方法中,使用 MethodDelegation.to 委托到成员方法
    intercept(MethodDelegation.to(new DelegateClazz()) // 委托到 DelegateClazz 的实例方法

4.4.2 委托到静态方法

在这里插入图片描述

上面的 intercept(MethodDelegation.to(DelegateClazz.class)) 是一个委托操作,一段委托函数,真正去执行输出被委托的函数方法。

被委托的方法,需要是 public 类
被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上
输出的class 文件,反编译过来的 java 文件,idea打开 如下:
在这里插入图片描述

那么此时就可以输出我们需要的内容了,

在这里插入图片描述

委托并不是根据名称来的,而是和 Java 编译器在选重载时用的参数绑定类似

4.4.3 委托到动态方法

前面示例中要委托到 DelegateClazz 的静态方法,这里要委托到 DelegateClazz 的实例方法需要在 MethodDelegation.to() 方法中传递


intercept(MethodDelegation.to(DelegateClazz.class)) // 委托到 Interceptor的静态方法
    
MethodDelegation.to(new DelegateClazz()) // 委托到 DelegateClazz 的实例方法

4.4.4 通过反射执行方法

这个和bytebuddy已经没有太多关系了

通过getLoaded(),可以拿到 bytebuddy 生产的字节码锁加载之后的 class 对象

然后通过class对象的反射机制,为了可以让整个方法运行起来,

我们需要添加字节码加载和反射调用的代码块,如下;

// 加载类clazz
Class<?> clazz = type.getLoaded();

// 反射调用
try {
    String bar = (String) clazz.getMethod("foo").invoke(clazz.newInstance());
    System.out.println(bar);
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
}

在这里插入图片描述

4.5 注解方法

除了通过上述 API 拦截方法并将方法实现委托给 Interceptor 增强之外,Byte Buddy 还提供了一些预定义的注解,

通过这些注解我们可以告诉 Byte Buddy 将哪些需要的数据注入到 Interceptor 中

4.5.1 常用注解含义

  • @RuntimeType 注解:
    告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。

  • @This 注解:
    注入被拦截的目标对象。

  • @AllArguments 注解:
    注入目标方法的全部参数,是不是感觉与 Java 反射的那套 API 有点类似了?

  • @Origin 注解:
    注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。

  • @Super 注解:
    注入目标对象。通过该对象可以调用目标对象的所有方法。

  • @SuperCall:
    这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,

    @SuperCall与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。

    另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

4.5.1 使用注解的例子

import org.junit.Test;

import java.lang.reflect.InvocationTargetException;

public static class DelegeteFoo {
    public String hello(String name) {
        System.out.println("DelegeteFoo:" + name);
        return null;
    }
}

public static class Interceptor {
    @RuntimeType
    public Object intercept(
            @This Object obj, // 目标对象
            @AllArguments Object[] allArguments, // 注入目标方法的全部参数
            @SuperCall Callable<?> zuper, // 调用目标方法,必不可少哦
            @Origin Method method, // 目标方法
            @Super DelegeteFoo delegeteFoo // 目标对象
    ) throws Exception {
        System.out.println("obj=" + obj);
        System.out.println("delegeteFoo =" + delegeteFoo);
        // 从上面两行输出可以看出,obj和db是一个对象
        try {
            return zuper.call(); // 调用目标方法
        } finally {
        }
    }

}

@Test
public void annotateDelegateTest() throws IllegalAccessException, InstantiationException {

    DynamicType.Unloaded<DelegeteFoo> dynamicType = new ByteBuddy()
            .subclass(DelegeteFoo.class)
            .name("com.crazymaker.circle.bytecode.enhancement.Foo")
            .method(named("hello"))
            .intercept(MethodDelegation.to(new Interceptor()))
            .make();

    // 加载字节码
    DynamicType.Loaded<DelegeteFoo> type = dynamicType.load(getClass().getClassLoader());

    // 输出类字节码
    outputClazz(dynamicType.getBytes(), "com.crazymaker.circle.bytecode.enhancement.Foo");

    // 加载类
    Class<?> clazz = type.getLoaded();

    // 反射调用
    try {
        String bar = (String) clazz.getMethod("hello", String.class).invoke(clazz.newInstance(), "bar - from 疯狂创客圈");
        System.out.println(bar);

    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

}

4.5.2 @Morph与@SuperCall

@SuperCall 注解注入的 Callable 参数来调用目标方法时,是无法动态修改参数的,

如果想要动态修改参数,则需要用到 @Morph 注解以及一些绑定操作

  DynamicType.Unloaded<DelegeteFoo> dynamicType = new ByteBuddy()
                .subclass(DelegeteFoo.class)
                .name("com.crazymaker.circle.bytecode.enhancement.Foo")
                .method(named("hello"))
                .intercept(MethodDelegation.withDefaultConfiguration()
                .withBinders(
                        // 要用@Morph注解之前,需要通过 Morph.Binder 告诉 Byte Buddy
                        // 要注入的参数是什么类型
                        Morph.Binder.install(OverrideCallable.class)
                ).to(new InterceptorMorph()))
                .make();

Interceptor 会使用 @Morph 注解注入一个 OverrideCallable 对象作为参数,然后通过该 OverrideCallable 对象调用目标方法

 public  static   class InterceptorMorph {
        @RuntimeType
        public Object intercept(@This Object obj,
                                @AllArguments Object[] allArguments,// 注入目标方法的全部参数
                                @Origin Method method,
                                @Super DelegeteFoo delegeteFoo,
                                @Morph OverrideCallable callable // 通过@Morph注解注入
        ) throws Throwable {
            try {
                System.out.println("obj="+obj);
                System.out.println("delegeteFoo ="+ delegeteFoo);
                System.out.println("method ="+method);
                System.out.println("callable ="+callable);
                System.out.println("allArguments ="+allArguments);
                System.out.println("before");
                // 通过 OverrideCallable.call()方法调用目标方法,此时需要传递参数
                allArguments[0]="word replaced";
                Object result = callable.call(allArguments);
                System.out.println("result ="+result);
                System.out.println("after");
                return result;
            } catch (Throwable t) {
                throw t;
            } finally {
                System.out.println("finally");
            }
        }
    }

最后,这里使用的 OverrideCallable 是一个自定义的接口,如下所示:

public interface OverrideCallable {
    Object call(Object[] args);
}

4.6 拦截构造函数

除了拦截 static 方法和实例方法,Byte Buddy 还可以拦截构造方法,这里依然通过一个示例进行说明。

拦截构造方法的步骤:

  • 使用 constructor() 方法拦截构造方法,

  • 并且使用 SuperMethodCall 调用构造方法并委托给 Interceptor 实例,

首先修改 DelegeteFoo 这个类,为它添加一个构造方法,如下所示:


public   static  class DelegeteFoo {

    public DelegeteFoo(String name) {
        System.out.println(" 构造器 DelegeteFoo : " +name);
    }
    public String hello(String name) {
        System.out.println("DelegeteFoo:" + name);
        return " result from DelegeteFoo ";
    }
}

使用的 Interceptor 与前文使用的类似:

class ConstructorInterceptor {
    @RuntimeType
    public void intercept(@This Object obj,
                          @AllArguments Object[] allArguments) {
        System.out.println("after!");
    }
}

这里不再使用 method() 方法拦截,而是使用 constructor() 方法拦截构造方法,

并且使用 SuperMethodCall 调用构造方法并委托给 Interceptor 实例,具体实现如下:

@Test
public void constructorInterceptTest() throws IllegalAccessException, InstantiationException {

    DynamicType.Unloaded<DelegeteFoo> dynamicType = new ByteBuddy()
            .subclass(DelegeteFoo.class)
            .name("com.crazymaker.circle.bytecode.enhancement.Foo")
            .constructor(any())
            // 通过constructor()方法拦截所有构造方法
            // 拦截的操作:首先调用目标对象的构造方法,根据前面自动匹配,
            // 这里直接匹配到参数为String.class的构造方法
            .intercept(SuperMethodCall.INSTANCE.andThen(
                    // 执行完原始构造方法,再开始执行interceptor的代码
                    MethodDelegation.withDefaultConfiguration().to(new ConstructorInterceptor())
            ))

            .make();

    // 加载字节码
    DynamicType.Loaded<DelegeteFoo> type = dynamicType.load(getClass().getClassLoader(), INJECTION);

    // 输出类字节码
    outputClazz(dynamicType.getBytes(), "com.crazymaker.circle.bytecode.enhancement.Foo");


    //加载类
    Class<?> clazz = type.getLoaded();

    // 反射调用
    try {

        Constructor<?> constructor = clazz.getConstructor(String.class);
        DelegeteFoo foo = (DelegeteFoo) constructor.newInstance("name from 疯狂创客圈");
        System.out.println(foo.hello("hello form 疯狂创客圈"));

    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
}

SuperMethodCall 会在新生成的方法中先调用目标方法,

如果未找到目标方法则抛出异常,如果目标方法是构造方法,则根据方法签名匹配。

输出如下

构造器 DelegeteFoo : name from 疯狂创客圈
after constructor!
DelegeteFoo:hello form 疯狂创客圈
 result of DelegeteFoo 

4.7 拦截实例通过 bytebuddy 进行耗时计算

拦截器代码

package com.crazymaker.agent.demo.bytebuddy;

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.concurrent.Callable;

public class MethodCostTime {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        long start = System.currentTimeMillis();
        try {
            // 原有函数执行
            return callable.call();
        } finally {
            System.out.println(method + " 方法耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
    }

}

拦截器使用

package com.crazymaker.agent.demo.bytebuddy;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.utility.JavaModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.instrument.Instrumentation;

import static net.bytebuddy.matcher.ElementMatchers.*;

public class ByteBuddyAgentDemo {

    private static final Logger log = LoggerFactory.getLogger(ByteBuddyAgentDemo.class);

    private final static String scanPackage = "com.crazymaker.circle.agent.demo";

    private final static String targetMethod = "sayHello";

//    private final static String implInterface = "org.springframework.cloud.gateway.filter.GlobalFilter";


    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println(">>>>> ByteBuddyAgentDemo - premain()");
        final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(false));
        new AgentBuilder.Default(byteBuddy)
                .type(nameStartsWith(scanPackage))
                .transform(new Transformer()) // update the byte code
                .with(new Listener())
                .installOn(inst);
    }

    /**
     *
     */
    private static class Transformer implements AgentBuilder.Transformer {
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
            if (typeDescription.getPackage().getActualName().equals(scanPackage)
//                    && typeDescription.getInterfaces().size() > 0
//                    && typeDescription.getInterfaces().get(0).getActualName().equals(implInterface)
                    ) {
                String targetClassName = typeDescription.getSimpleName();
                System.out.println("----------------------- target class:" + targetClassName);

                // 委托
                return builder.method(named(targetMethod)
                        .and(isPublic())).intercept(MethodDelegation.to(MethodCostTime.class));

            }
            return builder;
        }
    }

    /**
     * Listener
     */
    private static class Listener implements AgentBuilder.Listener {

        private int count;

        @Override
        public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
            if (typeName.startsWith(scanPackage)) {
                System.out.println("--- onDiscovery ---" + typeName);
            }
        }

        @Override
        public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {
            if (typeDescription.getSimpleName().startsWith(scanPackage)) {
                System.out.println("--- onTransformation ---" + typeDescription.getSimpleName());
            }
        }

        @Override
        public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {
        }

        @Override
        public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {
            if (typeName.startsWith(scanPackage)) {
                System.out.println("--- onError ---" + throwable);
            }
        }

        @Override
        public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
            if (typeName.startsWith(scanPackage)) {
                System.out.println("--- onComplete ---" + typeName);
            }
        }
    }

}

测试效果


/**
    * VM options:
    * -javaagent:D:\dev\SuperAPM\apm-agent\target\byteBuddy-demo.jar
    */

@Test
public void byteBuddyAgentDemo() throws IllegalAccessException, InstantiationException {

    new Helloworld().sayHello();
    System.out.println("hi byteBuddy AgentDemo ----------");

}

在这里插入图片描述