これは、なにをしたくて書いたもの?
Javaでバイトコードを生成したり操作するライブラリーにはいくつかありますが、今回はByte Buddyというものを試して
みたいと思います。
Byte Buddy
Byte BuddyのWebサイトはこちら。
Byte Buddy - runtime code generation for the Java virtual machine
GitHub - raphw/byte-buddy: Runtime code generation for the Java virtual machine.
特徴は以下のようです。
- Javaアプリケーションの実行時にコード生成、操作ができる
- Javaのバイトコードやクラスファイルの形式を理解している必要はない
- Java 5以上で動作する
- Javaのバイトコードを操作できるフレームワークである、ASMを使って構築されている
つまり、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
まずはチュートリアルを見て進めていこうと思います。
やってみて気づいたのですが、クラスの再定義・リベースは最初から扱うと話がややこしそうだったので、基本となりそうな
サブクラスの作成からやろうと思います。
これがなにを言っているのかはあとで書きます。
確認用のテストコードの雛形。
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!"); }
その場でクラスを作って、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"); }
ひとまずこんなところでしょうか。
今回はドキュメントを元に進めていきましたが、テストコードも参考になりそうです。
おわりに
Byte Buddyでバイトコードを生成・操作の初歩的なことをやってみました。
本当はクラスの再定義などをしたかったのですが、思った以上にてこずりそうなのとエントリーとしてまとらなくなりそうなので
今回はここで区切ることにしました。
再定義やリベースは、また次の機会に見ていこうと思います。