CLOVER🍀

That was when it all began.

Byte Buddyでバイトコードを生成・操作してみる

これは、なにをしたくて書いたもの?

Javaバイトコードを生成したり操作するライブラリーにはいくつかありますが、今回はByte Buddyというものを試して
みたいと思います。

Byte Buddy

Byte BuddyのWebサイトはこちら。

Byte Buddy - runtime code generation for the Java virtual machine

GitHubリポジトリーはこちら。

GitHub - raphw/byte-buddy: Runtime code generation for the Java virtual machine.

特徴は以下のようです。

つまり、ASMをさらに抽象化して比較的簡単に(?)Javaバイトコードを操作できるようにしたライブラリーと捉えると
よさそうです。

サポートしているJavaのバージョンは下限は6ですが、それ以上についてはこちらを見て対応するバージョンを選ぶことに
なるようです。

Byte Buddy / Java version compatibility

ドキュメントはGitHubリポジトリーのREADME.md、それからチュートリアルのようです。

ひとまず、使ってみましょう。

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.1 2025-10-21
OpenJDK Runtime Environment (build 25.0.1+8-Ubuntu-124.04)
OpenJDK 64-Bit Server VM (build 25.0.1+8-Ubuntu-124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.11 (3e54c93a704957b63ee3494413a2b544fd3d825b)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 25.0.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-90-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など。

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

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

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>6.0.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.27.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

JUnitやAssertJはテストを使った動作確認用です。

初めてのByte Buddy

まずはチュートリアルを見て進めていこうと思います。

Why runtime code generation?

やってみて気づいたのですが、クラスの再定義・リベースは最初から扱うと話がややこしそうだったので、基本となりそうな
サブクラスの作成からやろうと思います。

これがなにを言っているのかはあとで書きます。

確認用のテストコードの雛形。

src/test/java/org/littlewings/bytebuddy/ByteBuddyTutorialTest.java

package org.littlewings.bytebuddy;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ByteBuddyTutorialTest {

    // ここにテストを書く!
}

まずはこちらから。サブクラスの作成ですね。

Why runtime code generation? / Creating a class

といっても、Objectクラスのサブクラスなのでふつうにクラスを定義したのに近いですね。

    @Test
    void createSubClass() {
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .make();

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        assertThat(clazz.getName()).startsWith("net.bytebuddy.renamed.java.lang.Object$ByteBuddy$");
    }

Byte Buddyのエントリーポイントは、ByteBuddyというクラスになるようです。

ByteBuddyのメソッドを呼び出すと、ByteBuddyインスタンスだったりDynamicType.Builderが返ってきます。

DynamicType.Builder (Byte Buddy (without dependencies) 1.18.2 API)

というわけで、クラスを定義。

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

そしてクラスとしてロードします。

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

デフォルトでは作成されたクラスの名前はこのような雰囲気になるようです(前方一致でテストは書いています)。

        assertThat(clazz.getName()).startsWith("net.bytebuddy.renamed.java.lang.Object$ByteBuddy$");

名前を与えると、その名前のクラスになります。

    @Test
    void createNamedType() {
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("org.littlewings.Type")
                .make();

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        assertThat(clazz.getName()).isEqualTo("org.littlewings.Type");
    }

名前を与えない場合のデフォルトのネーミングでは、たとえばexample.Fooクラスのサブクラスを作成すると
example.Foo$$ByteBuddy$$1376491271となるようで、これを変えるにはこのようにします。

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

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        assertThat(clazz.getName()).isEqualTo("i.love.ByteBuddy.Object");
    }

サフィックスも変更できます。suffixを固定で、その後ろをランダムにしています。

    @Test
    void suffixNaming() {
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .with(new NamingStrategy.SuffixingRandom("suffix"))
                .subclass(Object.class)
                .make();

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        assertThat(clazz.getName()).startsWith("net.bytebuddy.renamed.java.lang.Object$suffix$");
    }

例として、net.bytebuddy.renamed.java.lang.Object$suffix$e5uAV9peといった感じのサフィックスになりました。

ちなみにここまで扱ってきたByteBuddyですが、これはスレッドセーフなようです。

Byte Buddy's API is expressed by fully immutable components and is therefore thread-safe.

ByteBuddy (Byte Buddy (without dependencies) 1.18.2 API)

ByteBuddyを返すメソッドは戻り値に対して操作を行う必要があるらしいので、新しいインスタンスが返ってきているの
でしょうね。

As a consequence, method calls must be chained for all of Byte Buddy's component, e.g. a method call like the following has no effect:

たとえば以下のような操作はNGです。

 ByteBuddy byteBuddy = new ByteBuddy();
 byteBuddy.foo()

正しくはこちら。

 ByteBuddy byteBuddy = new ByteBuddy().foo();

クラスの再定義とリベース

Byte Buddyを使うとサブクラスを作るだけでなく、既存のクラスを再定義(変更)したりリベースしたりできます。

リベースがよくわからなかったのですが、こういう定義があった時に

class Foo {
  String bar() { return "bar"; }
}

以下のように既存の実装は維持しつつ新しい処理をマージするのがリベースのようです。

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

再定義は文字どおり定義を置き換えます。

API上の使い方は、サブクラスを作成する時とほとんど同じです。

new ByteBuddy().subclass(Foo.class)
new ByteBuddy().redefine(Foo.class)
new ByteBuddy().rebase(Foo.class)

ただ実際に使おうとすると、javaagentが扱えないと難しそうなのでこのあたりはまたの機会に見ていこうと思います。

メソッドを変更してみる

次は、メソッドを変更してみましょう。

Why runtime code generation? / Fields and methods

README.mdに載っていた、toStringを固定値で返す例。

    @Test
    void methodReturnsFixedValue1() throws ReflectiveOperationException {
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .method(ElementMatchers.named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make();

        Class<?> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        assertThat(clazz.getConstructor().newInstance().toString()).isEqualTo("Hello World!");
    }

Byte Buddy / Usage

その場でクラスを作って、toStringメソッドを実装している感じですね。返す値は固定ですが。

Objectのサブクラスを作るだけだとイメージが湧きにくいので、こういうクラスを用意。

src/main/java/org/littlewings/bytebuddy/Foo.java

package org.littlewings.bytebuddy;

public class Foo {
    public String bar() {
        return "bar";
    }

    public String baz() {
        return "baz";
    }
}

Fooクラスのサブクラスを定義して、barメソッドは固定に返すようにしています。

    @Test
    void methodReturnsFixedValue2() throws ReflectiveOperationException {
        DynamicType.Unloaded<? extends Foo> dynamicType = new ByteBuddy()
                .subclass(Foo.class)
                .method(ElementMatchers.named("bar"))
                .intercept(FixedValue.value("Hello World!"))
                .make();

        Class<? extends Foo> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        Foo foo = clazz.getConstructor().newInstance();

        assertThat(foo.bar()).isEqualTo("Hello World!");
        assertThat(foo.baz()).isEqualTo("baz");
    }

次は、あるメソッドの呼び出しを別のメソッドに委譲するようにします。これにはMethodDelegationを使います。

MethodDelegation (Byte Buddy (without dependencies) 1.18.2 API)

こういうクラスを用意。

src/main/java/org/littlewings/bytebuddy/Foo2.java

package org.littlewings.bytebuddy;

public class Foo2 {
    public static String hoge() {
        return "Foo2#hoge";
    }
}

これで、Foo#barの呼び出しをFoo2#hogeに委譲できます。

    @Test
    void staticMethodDelegation() throws ReflectiveOperationException {
        DynamicType.Unloaded<? extends Foo> dynamicType = new ByteBuddy()
                .subclass(Foo.class)
                .method(ElementMatchers.named("bar"))
                .intercept(MethodDelegation.to(Foo2.class))
                .make();

        Class<? extends Foo> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        Foo foo = clazz.getConstructor().newInstance();

        assertThat(foo.bar()).isEqualTo("Foo2#hoge");
        assertThat(foo.baz()).isEqualTo("baz");
    }

次はインスタンスメソッドへの委譲。

src/main/java/org/littlewings/bytebuddy/Foo3.java

package org.littlewings.bytebuddy;

public class Foo3 {
    public String hoge() {
        return "Foo3#hoge";
    }
}

こうなります。

    @Test
    void instanceMethodDelegation() throws ReflectiveOperationException {
        DynamicType.Unloaded<? extends Foo> dynamicType = new ByteBuddy()
                .subclass(Foo.class)
                .method(ElementMatchers.named("bar"))
                .intercept(MethodDelegation.to(new Foo3()))
                .make();

        Class<? extends Foo> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        Foo foo = clazz.getConstructor().newInstance();

        assertThat(foo.bar()).isEqualTo("Foo3#hoge");
        assertThat(foo.baz()).isEqualTo("baz");
    }

先ほどとの違いは、staticメソッドの場合はこうでしたが

                .intercept(MethodDelegation.to(Foo2.class))

インスタンスメソッドに委譲する場合はこうなります。

                .intercept(MethodDelegation.to(new Foo3()))

複数のメソッドを持つクラスを委譲先にすると

src/main/java/org/littlewings/bytebuddy/Foo4.java

package org.littlewings.bytebuddy;

public class Foo4 {
    public String method1() {
        return "Foo4#method1";
    }

    public String method2() {
        return "Foo4#method2";
    }
}

委譲先が一意に定まらなくなるのでクラスを定義できなくなります。

    @Test
    void multipleInstanceMethodDelegationBad() throws ReflectiveOperationException {
        assertThatThrownBy(() -> new ByteBuddy()
                .subclass(Foo.class)
                .method(ElementMatchers.named("bar"))
                .intercept(MethodDelegation.to(new Foo4()))
                .make())
                .isExactlyInstanceOf(IllegalArgumentException.class)
                .hasMessage("Cannot resolve ambiguous delegation of public java.lang.String org.littlewings.bytebuddy.Foo.bar() to public java.lang.String org.littlewings.bytebuddy.Foo4.method1() or public java.lang.String org.littlewings.bytebuddy.Foo4.method2()");
    }

ではどうするかというと、委譲先を指定します。

    @Test
    void multipleInstanceMethodDelegation() throws ReflectiveOperationException {
        DynamicType.Unloaded<? extends Foo> dynamicType = new ByteBuddy()
                .subclass(Foo.class)
                .method(ElementMatchers.named("bar"))
                .intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("method1")).to(new Foo4()))
                .method(ElementMatchers.named("baz"))
                .intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("method2")).to(new Foo4()))

                .make();

        Class<? extends Foo> clazz = dynamicType.load(getClass().getClassLoader()).getLoaded();

        Foo foo = clazz.getConstructor().newInstance();

        assertThat(foo.bar()).isEqualTo("Foo4#method1");
        assertThat(foo.baz()).isEqualTo("Foo4#method2");
    }

ひとまずこんなところでしょうか。

今回はドキュメントを元に進めていきましたが、テストコードも参考になりそうです。

https://github.com/raphw/byte-buddy/blob/byte-buddy-1.18.2/byte-buddy-dep/src/test/java/net/bytebuddy/ByteBuddyTutorialExamplesTest.java

おわりに

Byte Buddyでバイトコードを生成・操作の初歩的なことをやってみました。

本当はクラスの再定義などをしたかったのですが、思った以上にてこずりそうなのとエントリーとしてまとらなくなりそうなので
今回はここで区切ることにしました。

再定義やリベースは、また次の機会に見ていこうと思います。