これは、なにをしたくて書いたもの?
前にByte Buddyを使ったり、Java agentについて見てみたりしてみました。
Byte Buddyでバイトコードを生成・操作してみる - CLOVER🍀
今度はByte Buddyを使って、Java agentを書いてみたいと思います。
ヒント
Byte BuddyでJava agentを書くにあたり、なにを参考にすればいいのかというところですが、ドキュメントには
ほとんど書かれていません。
こちらの中にある「Creating Java agents」というセクションだけですね。
Why runtime code generation? / Creating a class
ドキュメントを見ると、AgentBuilderというクラスを使うようです。
AgentBuilder (Byte Buddy (without dependencies) 1.18.3 API)
Byte Buddyにはいくつかのアーティファクトがあり、以前JUnitのテストコード内でJava agentをインストール
した時にはbyte-buddy-agentというアーティファクトを使いました。
AgentBuilderはbyte-buddyに含まれているので、こちらを使うだけであればbyte-buddy-agentは入りません。
ドキュメントで出てくるように、実行中のJava VMに対してJava agentを組み込む際に必要になるアーティファクトの
ようです。
public static void main(String[] args) { premain("", ByteBuddyAgent.install()); System.out.println(new Bar()); }
今回は-javaagentを指定する方法にします。
環境
今回の環境はこちら。
$ 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.12 (848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1) 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"
Java agentを組み込むアプリケーションを作成する
まずはJava agentを組み込むアプリケーションが必要ですね。
今回は小さく作ります。
<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>
こちらを変更対象のクラスにしましょう。
src/main/java/org/littlewings/MessageService.java
package org.littlewings; public class MessageService { public String decorate(String message) { return "★★★ %s ★★★".formatted(message); } public String hello() { return "Hello"; } }
mainクラス。
src/main/java/org/littlewings/App.java
package org.littlewings; public class App { public static void main(String... args) { MessageService messageService = new MessageService(); String word = args.length > 0 ? args[0] : "word"; System.out.printf("decorate = %s%n", messageService.decorate(word)); System.out.printf("hello = %s%n", messageService.hello()); } }
パッケージングして実行。
$ mvn clean package $ java -cp target/sample-app-0.0.1-SNAPSHOT.jar org.littlewings.App decorate = ★★★ word ★★★ hello = Hello
これで準備はできました。
Byte Buddyを使ってJava agentを作成する
では、Byte Buddyを使ってJava agentを作ります。
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.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>org.littlewings.bytebuddy.MyAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </plugin> </plugins> </build>
作成したクラスはこちら。
src/main/java/org/littlewings/bytebuddy/MyAgent.java
package org.littlewings.bytebuddy; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import net.bytebuddy.agent.builder.AgentBuilder; 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 net.bytebuddy.utility.JavaModule; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.named("org.littlewings.MessageService")) .transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { return builder.method(ElementMatchers.named("hello")).intercept(FixedValue.value("HELLO?")); } }) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.named("decorate")) .intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("decorate")).to(MyAgent.class)) ) .installOn(inst); } public static String decorate(String message) { return "※※※ %s ※※※".formatted(message); } }
AgentBuilder.Defaultというのは、セルフインジェクションとrebaseを有効にして使える戦略のようです。
ドキュメントどおりなのですが、まずはこれを選ぶのがよいのでしょう。
By default, Byte Buddy ignores any types loaded by the bootstrap class loader and any synthetic type. Self-injection and rebasing is enabled.
new AgentBuilder.Default()
AgentBuilder.Default (Byte Buddy (without dependencies) 1.18.3 API)
typeで対象を絞り込み
.type(ElementMatchers.named("org.littlewings.MessageService"))
transformで変換内容を設定します。
.transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { return builder.method(ElementMatchers.named("hello")).intercept(FixedValue.value("HELLO?")); } })
初見だと引数の意味がわからない気もしますが、DynamicType.BuilderはByte Buddyで最初の方で使うクラスです。
DynamicType.Builder#makeを呼び出すと、DynamicType.Unloadedが返ってきます。
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World!")) .make();
DynamicType.Builder (Byte Buddy (without dependencies) 1.18.3 API)
つまり、この中は適用対象の型を絞り込んだ後のルールをByte Buddyの使い方で書けばよいのです。
ここではhelloメソッドの内容を固定値に書き換えています。
return builder.method(ElementMatchers.named("hello")).intercept(FixedValue.value("HELLO?"));
慣れたらLambda式で書いてもよいでしょう。ここでは今回定義したクラスの中にある別のメソッドに転送しています。
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method(ElementMatchers.named("decorate"))
.intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("decorate")).to(MyAgent.class))
)
最後にInstrumentationを使っておしまい。
.installOn(inst);
パッケージング。
$ mvn clean package
使ってみます。
$ java -cp target/sample-app-0.0.1-SNAPSHOT.jar -javaagent:/path/to/target/sample-agent-0.0.1-SNAPSHOT.jar org.littlewings.App WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::objectFieldOffset has been called by net.bytebuddy.dynamic.loading.ClassInjector$UsingUnsafe$Dispatcher$CreationAction (file:/home/kazuhira/study/java/clover/byte-buddy-examples/byte-buddy-agent/sample-agent/target/sample-agent-0.0.1-SNAPSHOT.jar) WARNING: Please consider reporting this to the maintainers of class net.bytebuddy.dynamic.loading.ClassInjector$UsingUnsafe$Dispatcher$CreationAction WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release decorate = ※※※ word ※※※ hello = HELLO?
動きました!
decorate = ※※※ word ※※※ hello = HELLO?
ですが、警告もたくさん出ていますね。これはByte BuddyがUnsafeを使っているからのようです。Java 24以降だと
警告されるようです。
これについてはIssueがありました。
sun.misc.Unsafe deprecations in Java 24 · Issue #1803 · raphw/byte-buddy · GitHub
-Dnet.bytebuddy.safe=trueというシステムプロパティーを使うと動作を変更できるようです。
$ java -cp target/sample-app-0.0.1-SNAPSHOT.jar -javaagent:/path/to/target/sample-agent-0.0.1-SNAPSHOT.jar -Dnet.bytebuddy.safe=true org.littlewings.App decorate = ※※※ word ※※※ hello = HELLO?
たぶん速度的にはUnsafeを使った方が速いと思うのですが、今回はノイズになるので-Dnet.bytebuddy.safe=trueを
付けておくことにします。
組み込み対象のライブラリーをJava agentの依存関係に入れられる?
組み込み対象のアプリケーション(というかライブラリー)をJava agent側に含めても呼び出せるのかどうか、
というのが気になりました。
試してみましょう。
組み込み対象のアプリケーションに、こういうクラスを追加。
src/main/java/org/littlewings/Foo.java
package org.littlewings; public class Foo { public static String delegate(String message) { return "!!! %s !!!".formatted(message); } }
インストール。
$ mvn clean install
Java agent側には依存関係を追加します。
<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.3</version> </dependency> <dependency> <groupId>org.littlewings</groupId> <artifactId>sample-app</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>org.littlewings.bytebuddy.MyAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </plugin> </plugins> </build>
Java agentで起動するクラス。
src/main/java/org/littlewings/bytebuddy/MyAgent.java
package org.littlewings.bytebuddy; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import net.bytebuddy.agent.builder.AgentBuilder; 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 net.bytebuddy.utility.JavaModule; import org.littlewings.Foo; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.named("org.littlewings.MessageService")) .transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { return builder.method(ElementMatchers.named("hello")).intercept(FixedValue.value("HELLO?")); } }) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.named("decorate")) .intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("delegate")).to(Foo.class)) ) .installOn(inst); } }
ここで転送先に、依存ライブラリーに追加したクラスに設定。
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method(ElementMatchers.named("decorate"))
.intercept(MethodDelegation.withDefaultConfiguration().filter(ElementMatchers.named("delegate")).to(Foo.class))
)
パッケージングして
$ mvn clean package
実行。
$ java -cp target/sample-app-0.0.1-SNAPSHOT.jar -javaagent:/path/to/target/sample-agent-0.0.1-SNAPSHOT.jar -Dnet.bytebuddy.safe=true org.littlewings.App decorate = !!! word !!! hello = HELLO?
呼び出せるものですね。
適用対象のクラスに下手に使ったりするとStackOverflowErrorになるなどもしましたが…使い方を間違えなければ
参照自体はできそうです。
あまりやらない方がいい気がしますが、押さえておきましょう。
おわりに
Byte Buddyを使ってJava agentを書いてみました。
けっこう構えていたのですが、これまでと同じようにDynamicType.Builderが使えることに気づくとそれほど
警戒するものでもないかもしれません。
これで実行時にクラスの変更ができるようになりました。