关于后端:Java字节码-ByteBuddy原理与使用下

55次阅读

共计 4590 个字符,预计需要花费 12 分钟才能阅读完成。

构建 Java Agent

在应用程序中很多时候都不不便间接批改代码,java agent 模式能够不必间接批改利用的代码就可能实现本人的性能。应用 ByteBuddy 能够让咱们很容易构建本人的 agent。事实上很多的开源 Agent 都是借助的 ByteBuddy 来实现的 Agent。对于 java agent 我后续会写一些文章来进一步深刻介绍相干内容,在此就不多赘述了。

解决泛型

Java 的泛型会在运行时进行类型擦除。然而,因为泛型类型可能被嵌入到任何 Java 类文件中,并由 Java 反射 API 对外裸露。所以将通用信息蕴含到生成的类中是有意义的。

由此种种,在子类化类、实现接口或申明字段或办法时,ByteBuddy 承受 Type 的参数而不是擦除的Class。也能够应用 TypeDescription.Generic.Builder 明确定义泛型类型。

字段和办法

上述的章节讲述了类的创立与批改,接下来就要讲讲字段和办法的解决了。其实在上文中咱们也曾经举过了相干的例子了。咱们援用了一个将类的办法替换成其余返回值的例子:

    new ByteBuddy()
            .subclass(Object.class)
            .method(ElementMatchers.named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make()
            .saveIn(new File("result"));

当初咱们认真的扫视上述的代码,在 method 办法中应用到了 ElementMatchers.named 办法,这个办法是 ElementMatchers 中定义的一系列办法的其中一种,这个类次要用于创立易于人类浏览的类和办法匹配机制。其中定义了大量的办法来助于定义类和办法。

例如:

named("toString").and(returns(String.class)).and(takesArguments(0))

上述代码就是形容的名称为 toString,返回值为String 且没有参数的办法

接下来来看一个简单的案例:

class Foo {public String bar() {return null;}
  public String foo() { return null;}
  public String foo(Object o) {return null;}
}
 
Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

在这个例子中定义了三个办法匹配,根据 ByteBuddy 的实现准则,上述的调用是基于堆栈的模式,因而在最初的 .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!")) 反而会被最先匹配,当他未匹配胜利时会以堆栈的程序顺次匹配。

如果不想笼罩办法,想要从新定义本人的办法,能够应用defineMethod,当然这也合乎上述的堆栈的执行程序。

深刻理解 FixedValue

在上文中咱们曾经有了一些应用 FixedValue 的例子了。顾名思义,FixedValue的作用是返回一个固定的提供的对象。

类会议如下两种形式记录这个对象:

  1. 固定值写入类的常量池。常量池次要记住类的属性,比方类名或者办法名。除了这些反射属性之外,常量池还有空间来存储在类的办法或字段中应用的任何字符串或原始值。除了字符串和原始值,类池还能够存储对其余类型的援用。
  2. 该值存储在类的动态字段中。所以一旦将类加载到 Java 虚拟机中,就必须为该字段调配给定值。

当你应用 FixedValue.value(Object) 时,ByteBuddy 会剖析参数的类型,并且存储下来(优先尝试第一种办法,不可行才会应用第二种办法)。然而请留神,如果值存储在类池中,则所选办法返回的实例可能具备不同的对象标识。这种时候就能够应用 FixedValue.reference(Object) 来始终将对象存储在动态字段中。

委托办法调用

在很多场景下应用 FixedValue 返回固定值显然是远远不够的。所以 ByteBuddy 提供了 MethodDelegation 来反对更加弱小的和自在的办法定义。

看这个例子:

    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(ClassLoader.getSystemClassLoader())
            .getLoaded()
            .newInstance()
            .hello("World");

    System.out.println(helloWorld);

在这个例子外面咱们把 Sourcehello办法委托给了 Target,因而程序输入了Hello World! 而不是null

为了实现上述的成果 MethodDelegation 会找到 Target 的所有能够调用的办法并且进行最佳匹配。在上述办法中因为只有一个办法,因而匹配非常简单,那么遇到简单的状况 MethodDelegation 会怎么进行匹配呢?咱们看下一个例子:

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(); }
}

这个例子中的 Target 有三个重载办法,咱们用这个类来进行测试。通过测试,最初输入的后果是Hello World!,可能有人会纳闷为什么连办法名都齐全不一样,这也能被委托吗?

这里就波及到了 ByteBuddy 的实现了。ByteBuddy 不要求指标办法和源办法同名,回看上述办法,显然最初绑定的是第一个 intercept 办法,这是为什么呢?首先,第二个办法入参为 int 显然无奈匹配,然而第一和第三个办法应该如何抉择,这就又波及到了外部的实现问题。ByteBuddy 模拟了 java 编译器绑定重载办法的实现形式,总是抉择“最具体”的类型来进行绑定。而 String 显然比 Object 更为具体,因而绑定到了第一个 intercept 办法。

MethodDelegation能够配合注解 @Argument 一起应用,@Argument能够通过配置参数的地位(排在第 n 个)来进行参数的绑定。实际上如果你没有配置此注解,ByteBuddy 也会依照注解绑定的形式来解决,例如:

void foo(Object o1, Object o2)

如果原始办法是这样的,那么 ByteBuddy 会进行如下的解析:

void foo(@Argument(0) Object o1, @Argument(1) Object o2)

第一个参数和第二个参数会被调配到对应的拦截器,如果被拦挡的办法少于两个参数,或者参数类型不能匹配,那么就舍弃拦挡办法。

MethodDelegation还能够配合很多的注解来解决不同的场景:

  1. @AllArguments:此配置为数组类型,蕴含所有源办法的参数。为此,所有源办法参数都必须是可调配给数组的类型。如果不是此办法在匹配时会被舍弃。
  2. @This:这个注解能够用于获取以后实例
  3. @Origin:此注解用于获取办法的签名,例如:

    public static String intercept(@Origin String method) {return "Hello" + method + "!";}

    这段代码会输入
    Hello public java.lang.String org.example.bytebuddy.test.Source.hello(java.lang.String)!

拜访成员变量

应用 FieldAccessor 能够拜访类成员变量,并且能够读写变量的值。

咱们能够通过 FieldAccessor.ofBeanProperty() 来为类构建 Java Bean 标准的 getset办法:

    new ByteBuddy()
            .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
            .name("org.example.bytebuddy.FieldTest")
            .defineField("myField", String.class, Visibility.PRIVATE)
            .defineField("myTest", String.class, Visibility.PRIVATE)
            .defineMethod("getMyField", String.class)
            .intercept(FieldAccessor.ofBeanProperty())
            .make()
            .saveIn(new File("result"));

当然如果须要自行定义 field 的绑定名称,能够通过 FieldAccessor.ofField 来指定:

    new ByteBuddy()
            .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
            .name("org.example.bytebuddy.FieldTest")
            .defineField("myField", String.class, Visibility.PRIVATE)
            .defineField("myTest", String.class, Visibility.PRIVATE)
            .defineMethod("getMyField", String.class)
            .intercept(FieldAccessor.ofField("myTest"))
            .make()
            .saveIn(new File("result"));

总结

ByteBuddy作为一种高性能的字节码组件有着较为宽泛的应用。他的能力十分弱小,此处只是介绍了他的局部能力,如果有需要的话能够返回 byte-buddy 理解更多信息。

正文完
 0