java

关注公众号 jb51net

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

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

作者:骑牛上青山

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

什么是ByteBuddy

ByteBuddy是一个java的运行时代码生成库,他可以帮助你以字节码的方式动态修改java类的代码。

为什么需要ByteBuddy

Java是一个强类型语言,有着极为严格的类型系统。这个严格的类型系统可以帮助构建严谨,更不容易被腐化的代码,但是也在某些方面限制了java的应用。不过为了解决这个问题,java提供了一套反射的api来帮助使用者感知和修改类的内部。

不过反射也有他的缺点:

ByteBuddy就可以帮助我们做到反射能做的事情,而不必受困于他的这些缺点。

ByteBuddy使用

创建一个类

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

上述代码创建了一个Object的子类并且创建了toString方法输出Hello World!通过找到保存的输出类我们可以看到最后的类是这样的:

package net.bytebuddy.renamed.java.lang;
public class Object$ByteBuddy$tPSTnhZh {
    public String toString() {
        return "Hello World!";
    }
    public Object$ByteBuddy$tPSTnhZh() {
    }
}

可以看到我们虽然创建了一个类,但是我们没有为这个类取名,通过结果得知最后的类名是
net.bytebuddy.renamed.java.lang.Object$ByteBuddy$tPSTnhZh,那么这个类名是怎么来的呢?

在ByteBuddy中如果没有指定类名,他会调用默认的NamingStrategy策略来生成类名,一般情况下为

父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: org.example.MyTest$ByteBuddy$NsT9pB6w

如果父类是java.lang目录下的类,例如Object,那么会变成

net.bytebuddy.renamed. + 父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: net.bytebuddy.renamed.java.lang.Object$ByteBuddy$2VOeD4Lh

以此来规避java安全模型的限制。

类型重定义与变基

定义一个类

package org.example.bytebuddy.test;
public class MyClassTest {
    public String test() {
        return "my test";
    }
}

用这个类来验证如下的能力

类型重定义(type redefinition)

ByteBuddy支持对于已存在的类进行重定义,即可以添加或者删除类的方法。只不过当类的方法被重定义之后,那么原先的方法中的信息就会丢失。

Class<?> dynamicType = new ByteBuddy()
                .redefine(MyClassTest.class)
                .method(ElementMatchers.named("test"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(String.class.getClassLoader()).getLoaded();

redefine结果是

类型变基(type rebasing)

rebase操作和redefinition操作最大的区别就是rebase操作不会丢失原先的类的方法信息。大致的实现原理是在变基操作的时候把所有的方法实现复制到重新命名的私有方法(具有和原先方法兼容的签名)中,这样原先的方法就不会丢失。

Class&lt;?&gt; dynamicType = new ByteBuddy()
                .rebase(MyClassTest.class)
                .method(ElementMatchers.named("test"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(String.class.getClassLoader()).getLoaded();

rebase之后结果

可以看到原先的方法被重命名后保留了下来,并且变成了私有方法。

注意redefinition和rebasing不能修改已经被jvm加载的类,不然会报错Class already loaded

类的加载

生成了之后为了在代码中使用,必须要经过load流程。细心的读者可能已经发现了上文中已经使用到了load相关的方法。

构建了具体的动态类之后,可以选择使用saveIn将其结构体存储下来,也可以选择将它装载到虚拟机中。在类加载器的选择中,ByteBuddy提供了几种选择放在ClassLoadingStrategy.Default中:

类的重载

前面提到过,rebase和redefine通常没办法重新加载已经存在的类,但是由于jvm的热替换(HotSwap)机制的存在,使得ByteBuddy可以在加载后也能够重新定义类。

class Foo {
  String m() { return "foo"; }
}
class Bar {
  String m() { return "bar"; }
}

我们通过ByteBuddy的ClassRelodingsTrategy即可完成热替换。

ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());

需要注意的是热替换机制必须依赖Java Agent才能使用。Java Agent是一种可以在java项目运行前或者运行时动态修改类的技术。通常可以使用-javaagent参数引入java agent。

处理尚未加载的类

ByteBuddy除了可以处理已经加载完的类,他也具备处理尚未被加载的类的能力。

ByteBuddy对java的反射api做了抽象,例如Class实例就被表示成了TypeDescription实例。事实上,ByteBuddy只知道如何通过实现TypeDescription接口的适配器来处理提供的 Class。这种抽象的一大优势是类信息不需要由类加载器提供,可以由任何其他来源提供。

ByteBuddy中可以通过TypePool获取类的TypeDescription,ByteBuddy提供了TypePool的默认实现TypePool.Default。这个类可以帮助我们把java字节码转换成TypeDescription

Java的类加载器只会在类第一次使用的时候加载一次,因此我们可以在java中以如下方式安全的创建一个类:

package foo;
class Bar { }

但是通过如下的方法,我们可以在Bar这个类没有被加载前就提前生成我们自己的Bar,因此后续jvm就只会使用到我们的Bar

参考文章

[1] https://bytebuddy.net/#/tutorial

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

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