字节码增强技术-Byte Buddy

简介

Byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。除了Java类库附带的代码生成实用程序外,Byte Buddy还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy提供了一种方便的API,可以使用Java代理或在构建过程中手动更改类。

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

性能

在选择字节码操作库时,往往需要考虑库本身的性能。对于许多应用程序,生成代码的运行时特性更有可能确定最佳选择。而在生成的代码本身的运行时间之外,用于创建动态类的运行时也是一个问题。官网对库进行了性能测试,给出以下结果图:
image.png

图中的每一行分别为,类的创建、接口实现、方法调用、类型扩展、父类方法调用的性能结果。从性能报告中可以看出,Byte Buddy 的主要侧重点在于以最少的运行时生成代码,需要注意的是,我们这些衡量 Java 代码性能的测试,都由 Java 虚拟机即时编译器优化过,如果你的代码只是偶尔运行,没有得到虚拟机的优化,可能性能会有所偏差。所以我们在使用 Byte Buddy 开发时,我们希望监控这些指标,以避免在添加新功能时造成性能损失。

使用

install

使用bytebuddy只需要简单的引入其maven依赖即可

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

Hello World

Class<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .method(ElementMatchers.named("toString"))
                .intercept(FixedValue.value("Hello World"))
                .make()
                .load(HelloWorldBuddy.class.getClassLoader())
                .getLoaded();
Object instance = dynamicType.newInstance();
String toString = instance.toString();
System.out.println(toString);
System.out.println(instance.getClass().getCanonicalName());

从例子可以看出,很简单就创建了一个动态类型。ByteBuddy提供了一套流式API,从ByteBuddy实例出发,可以流畅的完成所有的操作和数据定义。
上面的示例中

  • subclass 指定了新创建的类的父类
  • method 指定了 Object 的 toString 方法
  • intercept 拦截了 toString 方法并返回固定的 value
  • 最后 make 方法生产字节码,有类加载器加载到虚拟机中

此外,Byte Buddy不仅限于创建子类和操作类,还可以转换现有代码。Byte Buddy 还提供了一个方便的 API,用于定义所谓的 Java 代理,该代理允许在任何 Java 应用程序的运行期间进行代码转换

创建动态类

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("com.zlk.learning.bytebuddy.DynamicType")
  .make();

上面的示例代码会创建一个继承至 Object 类型的类。这个动态创建的类型与直接扩展 Object 并且没有实现任何方法、属性和构造函数的类型是等价的,如下:

public class DynamicTYpe {

}

在创建类的时候,还提供了更多API来支持对类的定义,包括定义字段、方法等

        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.zlk.learning.bytebuddy.DynamicType")
                .defineField("name", String.class, 1)
                .defineField("age", Integer.class, 1)
                .method(ElementMatchers.named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make();

上面的示例代码中,我们增加了两个字段name和age,同时拦截了toString方法,使其输出固定值 “Hello World!”。

加载类

上节创建的 DynamicType.Unloaded,代表一个尚未加载的类,顾名思义,这些类型不会加载到 Java 虚拟机中,它仅仅表示创建好了类的字节码,通过 DynamicType.Unloaded 中的 getBytes 方法你可以获取到该字节码。

在应用程序中,可能需要将该字节码保存到文件,或者注入的现在的 jar 文件中,因此该类型还提供了一个 saveIn(File) 方法,可以将类存储在给定的文件夹中; inject(File) 方法将类注入到现有的 Jar 文件中,另外你只需要将该字节码直接加载到虚拟机使用,你可以通过 ClassLoadingStrategy 来加载。

如果不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中

  • WRAPPER:创建一个新的Wrapping类加载器
  • CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
  • INJECTION:利用反射机制注入动态类型
 Class<?> dynamicClass = dynamicType
                .load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();

我们使用 WRAPPER 策略来加载适合大多数情况的类,这样生产的动态类不会被ApplicationClassLoader加载到,不会影响到项目中已经存在的类
getLoaded 方法返回一个 Java Class 的实例,它就表示现在加载的动态类

拦截方法

在之前的例子中,我们拦截了toString方法,并使其输出固定值。不过在实际开发中很少会遇到如此简单的场景,我们可以通过指定拦截方法的形式来处理复杂的逻辑

通过匹配模式拦截

ByteBuddy 通过 net.bytebuddy.matcher.ElementMatcher 来定义配置策略,可以通过此接口实现自己定义的匹配策略

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  // 匹配由Foo.class声明的方法
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  // 匹配名为foo的方法
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  // 匹配名为foo,入参数量为1的方法
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

方法委托

使用MethodDelegation可以将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

其中 Target 还可以如下实现:

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}

前一个实现因为只有一个方法,而且类型也匹配,很好理解,那么后一个呢,Byte Buddy到底会委托给哪个方法?Byte Buddy遵循一个最接近原则:

  • intercept(int)因为参数类型不匹配,直接Pass
  • 另外两个方法参数都匹配,但是 intercept(String)类型更加接近,因此会委托给它

同时需要注意的是被拦截的方法需要声明为 public,否则没法进行拦截增强。除此之外,还可以使用 @RuntimeType 注解来标注方法

@RuntimeType
public static Object intercept(@RuntimeType Object value) {
        System.out.println("Invoked method with: " + value);
        return value;
}

参数绑定

可以在拦截器(Target)的拦截方法 intercept 中使用注解注入参数,ByteBuddy 会根据注解给我们注入对于的参数值。比如

void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)

常用注解有以下这些:

  • @Argument 绑定单个参数
  • @AllArguments 绑定所有参数的数组
  • @This 当前被拦截的、动态生成的那个对象
  • @DefaultCall 调用默认方法而非super的方法
  • @SuperCall 用于调用父类版本的方法
  • @Origin 被拦截的源方法
  • @RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
  • @Super 当前被拦截的、动态生成的那个对象的父类对象
  • @FieldValue 注入被拦截对象的一个字段的值

Agent

Java 从 1.5 开始提供了 java.lang.instrument包,该包为检测 Java 程序提供 API,比如用于监控、收集性能信息、诊断问题。通过 java.lang.instrument 实现工具被称为 Java Agent。Java Agent 可以修改类文件的字节码,通常是,在字节码方法插入额外的字节码来完成检测

和通过ByteBuddy实例创建动态类型一样,bytebuddy也提供了AgentBuilder类使我们在agent中更优雅地编写代码

class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}
  • type 通过ElementMatcher 来匹配我们加载的class,匹配到之后,将会使用
  • transform 指定的转换器来对匹配到的class进行操作

ElementMatcher

ElementMatcher可以定义匹配class的规则,在bytebuddy中,ElementMatchers类提供了许多常规的匹配方式,可以按照class name、注解、类型等来进行匹配,上面的实例中就是使用注解匹配的方式

Junction继承自ElementMatcher接口,定义了and 和 or 方法,可以使我们在定义Matcher时通过链式定义一连串的匹配规则

      new AgentBuilder.Default()
                .type(ElementMatchers.isAnnotatedWith(ToString.class)).and(ElementMatchers.isSubTypeOf(DynamicClass.class)).or(ElementMatchers.named("DynamicClass"))
                .transform(new AgentBuilder.Transformer() {
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                        return builder
                                .method(ElementMatchers.named("hello"))
                                .intercept(MethodDelegation.to(MyServiceInterceptor.class))
                                ;
                    }
                }).installOn(instrumentation);

Transformer

Transformer 接口定义了 transform方法,会传入DynamicType.Builder实例,通过该builder,就可以对匹配到的类进行操作,就和上面讲的 ByteBuddy创建动态类型时类似操作,可以定义字段以及对方法进行拦截操作等,上面的例子就是对匹配到的类的hello方法进行了方法委托,在调用hello方法时,将会委托给 MyServiceInterceptor类

public class MyServiceInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        System.out.println("intercept:拦截了" + method.getName());
        return callable.call();
    }
    
}

END

参考文章
bytebuddy官方文档 https://bytebuddy.net/#/tutorial
https://juejin.cn/post/6844903965553852423#heading-12

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值