Byte Buddy 生成的类的结构如何?(第一篇)

38 阅读8分钟

背景

Byte Buddy 的 网站 上提供了 这样的页面

其中有一小节是 Creating a class

image.png 中文版的页面 里与之对应的是 类创建 小节

image.png

在那一小节,我们可以看到很多具体的例子。其中一个例子如下

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

我想看看 Byte Byddy 所生成的类的结构是怎样的,所以写了本文来进行记录。让我们开始探索吧。

正文

准备工作

Byte Buddy (without Dependencies) 页面,可以看到 Byte Buddy 的版本列表 ⬇️

image.png 我们就用最新的那个吧(版本号是 1.18.7)。可以在 Byte Buddy (without Dependencies) » 1.18.7 页面下载 jar 包 ⬇️

image.png

我将它下载到本地后,文件的名称是 byte-buddy-1.18.7.jar

第一个例子: Object 的子类

类创建 里,可以找到如下的例子

image.png

我把代码复制到下方了 ⬇️

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

为了行文的方便,本文就用 javac/java/javap 这些命令直接在命令行来进行操作了。但其实还是建一个专门的项目(例如 maven 项目)更容易处理各种问题,而且写代码也更方便。

我们把如下的代码保存到 Main.java 文件里。

import net.bytebuddy.ByteBuddy;

import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        new ByteBuddy()
                .subclass(Object.class)
                .make()
                .saveIn(new File("."));
    }
}

编译与运行

通过执行如下命令,就可以编译 Main.java (请注意要先把 byte-buddy-1.18.7.jar 移动到当前路径下)

javac -cp byte-buddy-1.18.7.jar Main.java

编译成功后,应该可以在当前目录下看到 Main.class 文件。然后再执行如下命令就可以运行 Main 类里的 main 方法。

java -cp byte-buddy-1.18.7.jar:. Main

运行后没有任何输出,这是符合预期的。因为 Main 类里的 main 方法并不会在标准输出里打印任何内容。此时执行 ls 命令,会发现当前目录下多了一个 net 目录。我在当前目录下执行 tree . 命令后,看到的结果如下 ⬇️

.
├── byte-buddy-1.18.7.jar
├── Main.class
├── Main.java
└── net
    └── bytebuddy
        └── renamed
            └── java
                └── lang
                    └── Object$ByteBuddy$i8uhbcjD.class

6 directories, 4 files

(也许 Byte Buddy 在你电脑上生成的 class 文件会是另一个名字)

看来 Object$ByteBuddy$i8uhbcjD.class 就是 Byte Buddy 所生成的 class 文件了。通过执行如下命令就可以查看它的内容 (在你的电脑上,class 文件的名称可能会有差异,请注意调整)

javap -v -p net.bytebuddy.renamed.java.lang.Object\$ByteBuddy\$i8uhbcjD

其输出如下 (我删掉了前三行)

public class net.bytebuddy.renamed.java.lang.Object$ByteBuddy$i8uhbcjD
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // net/bytebuddy/renamed/java/lang/Object$ByteBuddy$i8uhbcjD
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
   #1 = Utf8               net/bytebuddy/renamed/java/lang/Object$ByteBuddy$i8uhbcjD
   #2 = Class              #1             // net/bytebuddy/renamed/java/lang/Object$ByteBuddy$i8uhbcjD
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               Code
{
  public net.bytebuddy.renamed.java.lang.Object$ByteBuddy$i8uhbcjD();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
}

反编译的结果

由于这个 class 文件的内容并不复杂,我们可以尝试手动反编译。反编译的结果如下

// 以下内容是我反编译的结果,不保证绝对准确,仅供参考
package net.bytebuddy.renamed.java.lang;

public class Object$ByteBuddy$i8uhbcjD {
    public Object$ByteBuddy$i8uhbcjD() {
        super();
    }
}

类图

类图如下 ⬇️ image.png

第二个例子: 在第一个例子的基础上指定包名和类名

我们在 类创建 里继续看第二个例子

image.png

文章中提供的代码如下 ⬇️

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

我们稍作调整,可以写出如下的 Main2.java

import net.bytebuddy.ByteBuddy;

import java.io.File;
import java.io.IOException;

public class Main2 {
    public static void main(String[] args) throws IOException {
        new ByteBuddy()
                .subclass(Object.class)
  		.name("example.Type")
                .make()
                .saveIn(new File("."));
    }
}

编译与运行

执行如下命令就可以编译 Main2.java 以及运行其中的 main 方法 ⬇️

javac -cp byte-buddy-1.18.7.jar Main2.java
java -cp byte-buddy-1.18.7.jar:. Main2

此时再执行 tree . 命令,就会看到有新的目录/文件生成 ⬇️(我已经把第一个例子生成的目录/文件删掉了)

.
├── byte-buddy-1.18.7.jar
├── example
│   └── Type.class
├── Main.java
├── Main2.class
└── Main2.java

2 directories, 5 files

可见,Byte Buddy 生成了 example 目录,并在其中生成了 Type.class 文件。我们用如下的命令可以查看 Type.class 文件的内容

javap -v -p example.Type

其输出如下 (我删掉了前三行)

public class example.Type
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // example/Type
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
   #1 = Utf8               example/Type
   #2 = Class              #1             // example/Type
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               Code
{
  public example.Type();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
}

反编译的结果

由于这个 class 文件的内容并不复杂,我们可以尝试手动反编译。反编译的结果如下

// 以下内容是我反编译的结果,不保证绝对准确,仅供参考
package example;

public class Type {
    public Type() {
        super();
    }
}

类图

类图如下 ⬇️

image.png

第三个例子: 指定命名策略

我们在 类创建 里继续看第三个例子

image.png

文章中提供的代码如下 ⬇️

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    protected String name(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

我们稍作调整,可以写出如下的 Main3.java

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.type.TypeDescription;

import java.io.File;
import java.io.IOException;

public class Main3 {

    public static void main(String[] args) throws IOException {
        new ByteBuddy()
                .with(new NamingStrategy.AbstractBase() {
                    @Override
                    protected String name(TypeDescription superClass) {
                        return "i.love.ByteBuddy." + superClass.getSimpleName();
                    }
                })
                .subclass(Object.class)
                .make()
                .saveIn(new File("."));
    }
}

编译与运行

执行如下命令就可以编译 Main3.java 以及运行其中的 main 方法 ⬇️

javac -cp byte-buddy-1.18.7.jar Main3.java
java -cp byte-buddy-1.18.7.jar:. Main3

此时再执行 tree . 命令,就会看到有新的目录/文件生成 ⬇️(我已经把前两个例子生成的目录/文件删掉了)

.
├── byte-buddy-1.18.7.jar
├── i
│   └── love
│       └── ByteBuddy
│           └── Object.class
├── Main.java
├── Main2.java
├── Main3.class
├── Main3.java
└── Main3$1.class

4 directories, 7 files

因为我们的代码里用到了匿名内部类,所以会生成 Main3$1.class 文件。而 i/love/ByteByddy/Object.class 看起来是 Byte Buddy 生成的。我们用如下的命令可以查看 Type.class 文件的内容

javap -v -p i/love/ByteBuddy/Object.class

其输出如下 (我删掉了前三行)

public class i.love.ByteBuddy.Object
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // i/love/ByteBuddy/Object
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
   #1 = Utf8               i/love/ByteBuddy/Object
   #2 = Class              #1             // i/love/ByteBuddy/Object
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               Code
{
  public i.love.ByteBuddy.Object();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
}

反编译的结果

由于这个 class 文件的内容并不复杂,我们可以尝试手动反编译。反编译的结果如下

// 以下内容是我反编译的结果,不保证绝对准确,仅供参考
package i.love.ByteBuddy;

public class Object {
    public Object() {
        super();
    }
}

类图

类图如下 ⬇️

image.png

第四个例子:使用指定后缀的命名策略

由于 类创建 里的第四个例子的代码有问题,我们去 Creating a class 看第四个例子 👇

image.png

请注意,第四个例子里展示的是一种错误的写法。第四个例子的代码复制如下 👇

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

改为正确的写法后,再稍作调整,可以写出如下的 Main4.java

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;

import java.io.File;
import java.io.IOException;

public class Main4 {
    public static void main(String[] args) throws IOException {
        new ByteBuddy().with(new NamingStrategy.SuffixingRandom("suffix"))
                .subclass(Object.class)
                .make()
                .saveIn(new File("."));
    }
}

编译与运行

执行如下命令就可以编译 Main4.java 以及运行其中的 main 方法 ⬇️

javac -cp byte-buddy-1.18.7.jar Main4.java
java -cp byte-buddy-1.18.7.jar:. Main4

此时再执行 tree . 命令,就会看到有新的目录/文件生成 ⬇️

.
├── byte-buddy-1.18.7.jar
├── Main4.class
├── Main4.java
└── net
    └── bytebuddy
        └── renamed
            └── java
                └── lang
                    └── Object$suffix$GdffAyGB.class

6 directories, 4 files

这次生成的 class 文件果然使用了我们指定的后缀 "suffix"。我们用如下的命令可以查看这个 class 文件的内容(请注意,在您的电脑上生成的 class 文件的名称可能与它不同)

javap -v -p net.bytebuddy.renamed.java.lang.Object\$suffix\$GdffAyGB

其输出如下 (我删掉了前三行)

public class net.bytebuddy.renamed.java.lang.Object$suffix$GdffAyGB
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // net/bytebuddy/renamed/java/lang/Object$suffix$GdffAyGB
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
   #1 = Utf8               net/bytebuddy/renamed/java/lang/Object$suffix$GdffAyGB
   #2 = Class              #1             // net/bytebuddy/renamed/java/lang/Object$suffix$GdffAyGB
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               Code
{
  public net.bytebuddy.renamed.java.lang.Object$suffix$GdffAyGB();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
}

反编译的结果

由于这个 class 文件的内容并不复杂,我们可以尝试手动反编译。反编译的结果如下

// 以下内容是我反编译的结果,不保证绝对准确,仅供参考
package net.bytebuddy.renamed.java.lang;

public class Object$suffix$GdffAyGB {
    public Object$suffix$GdffAyGB() {
        super();
    }
}

类图

类图如下 ⬇️

image.png

其他

用 PlantUML 画图,所用到的代码列举如下

画 "第一个例子的类图" 所用到的代码

@startuml

title 第一个例子的类图

class net.bytebuddy.renamed.java.lang.Object$ByteBuddy$i8uhbcjD {
    + Object$ByteBuddy$i8uhbcjD()
}

note right of net.bytebuddy.renamed.java.lang.Object$ByteBuddy$i8uhbcjD::Object$ByteBuddy$i8uhbcjD
这是构造函数
end note

@enduml

画 "第二个例子的类图" 所用到的代码

@startuml

title 第二个例子的类图

class example.Type {
    + Type()
}

note right of example.Type::Type
这是构造函数
end note

@enduml

画 "第三个例子的类图" 所用到的代码

@startuml

title 第三个例子的类图

class i.love.ByteBuddy.Object {
    + Object()
}

note right of i.love.ByteBuddy.Object::Object
这是构造函数
end note

@enduml

画 "第四个例子的类图" 所用到的代码

@startuml

title 第四个例子的类图

class net.bytebuddy.renamed.java.lang.Object$suffix$GdffAyGB {
    + Object$suffix$GdffAyGB()
}

note right of net.bytebuddy.renamed.java.lang.Object$suffix$GdffAyGB::Object$suffix$GdffAyGB
这是构造函数
end note

@enduml

参考资料

Byte Buddy 的 网站 上的