java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java字节码ByteBuddy

Java字节码ByteBuddy使用及原理解析下

作者:骑牛上青山

这篇文章主要为大家介绍了Java字节码ByteBuddy使用及原理解析下篇,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

构建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的作用是返回一个固定的提供的对象。

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

当你使用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还可以配合很多的注解来处理不同的场景:

@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了解更多信息。

以上就是Java字节码ByteBuddy使用及原理解析下的详细内容,更多关于Java字节码ByteBuddy的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文