これは、なにをしたくて書いたもの?
最近Byte Buddyを扱っていましたが、今回でひと区切りにしたいと思います。
今回はByte BuddyのAdviceを使ってJava agentを書いてみます。
Byte BuddyのAdvice
Byte BuddyのAdviceというのはこちらです。
Advice (Byte Buddy (without dependencies) 1.18.3 API)
Adviceは、マッチしたメソッドの前後に実行されるメソッドのコードをコピーします。
Advice wrappers copy the code of blueprint methods to be executed before and/or after a matched method.
@Advice.OnMethodEnterアノテーションおよび@Advice.OnMethodExitアノテーションを、staticメソッドに
付与することで機能します。
To achieve this, a static method of a class is annotated by Advice.OnMethodEnter and/or Advice.OnMethodExit and provided to an instance of this class.
これらを使うことで対象のメソッドの前後に処理を行うことができます。インターセプターのようなものを
作れる、という感じですね。
メソッドの呼び出し前に処理を行うには@Advice.OnMethodEnterアノテーションを、呼び出し後に処理を
行うには@Advice.OnMethodExitアノテーションを使います。
Advice.OnMethodEnter (Byte Buddy (without dependencies) 1.18.3 API)
Advice.OnMethodExit (Byte Buddy (without dependencies) 1.18.3 API)
これらのアノテーションはメソッドに付与しますが、インライン化されるためstaticメソッドである必要が
あります。
使い方はAdviceクラスのJavadocに書かれているのですが、以下のようなアノテーションが使えます。
@Advice.Argument… メソッドの引数を割り当てる@Advice.AllArguments… メソッドの引数すべてを割り当てる@Advice.This… instrumentされたthisの参照を割り当てる@Advice.FieldValue… instrumentされたフィールドを割り当てる@Advice.Return… メソッドの戻り値を割り当てる@Advice.OnMethodExitのみで使用可能- Advice.Return (Byte Buddy (without dependencies) 1.18.3 API)
@Advice.Thrown… メソッドからスローされた例外に割り当てる@Advice.OnMethodExitのみで使用可能でonThrowable属性を指定する必要がある- Advice.Thrown (Byte Buddy (without dependencies) 1.18.3 API)
@Advice.Origin… instrument対象のメソッドの文字列表現、またはMethod、Constructorなどに割り当てる@Advice.Enter…@Advice.OnMethodEnterを付与したメソッドの戻り値を@Advice.OnMethodExitを付与したメソッドで取得できる
参考)
My daily Java: Using Byte Buddy for proxy creation
ただ、説明を見てもわからない気もするので実際に使ってみるとしましょう。
今回はJava agentとして作ります。
Java agentとByte Buddyの使い方については以前のエントリーを参照してください。
Byte Buddyを使ってJava agentを書いてみる - CLOVER🍀
環境
今回の環境はこちら。
$ 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を組み込むアプリケーションを用意しましょう。
pom.xmlはビルド設定くらいのシンプルなものです。
<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>
mainメソッドを持ったクラス。起動時の引数の数で、呼び出すクラスを分けています。
src/main/java/org/littlewings/App.java
package org.littlewings; public class App { public static void main(String... args) { if (args.length == 1) { MessageService messageService = new MessageService("★"); System.out.printf("message = %s%n", messageService.decorate(args[0])); } else if (args.length == 2) { MessageService2 messageService2 = new MessageService2("※"); System.out.printf("message = %s%n", messageService2.decorate(args[0], args[1])); } else { System.out.println("please 1 or 2 arguments"); } } }
呼び出されるクラス。これらがAdviceを使う対象になります。
src/main/java/org/littlewings/MessageService.java
package org.littlewings; public class MessageService { private String c; public MessageService(String c) { this.c = c; } public String decorate(String message) { System.out.println("===== call MessageService#decorate ====="); return "%s%s%s %s %s%s%s".formatted(c, c, c, message, c, c, c); } }
src/main/java/org/littlewings/MessageService2.java
package org.littlewings; public class MessageService2 { private String c; public MessageService2(String c) { this.c = c; } public String decorate(String message1, String message2) { System.out.println("===== call MessageService2#decorate ====="); if ("throw".equalsIgnoreCase(message1) && "exception".equalsIgnoreCase(message2)) { throw new IllegalArgumentException("Oops!!"); } return "%s%s%s %s %s!! %s%s%s".formatted(c, c, c, message1, message2, c, c, c); } }
2つ目のクラスは、特定のキーワードを渡すと例外をスローします。
ビルドして
$ mvn package
実行。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App hello ===== call MessageService#decorate ===== message = ★★★ hello ★★★ $ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App Hello World ===== call MessageService2#decorate ===== message = ※※※ Hello World!! ※※※ $ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App throw exception ===== call MessageService2#decorate ===== Exception in thread "main" java.lang.IllegalArgumentException: Oops!! at org.littlewings.MessageService2.decorate(MessageService2.java:14) at org.littlewings.App.main(App.java:10)
これで準備はできました。
Byte BuddyのAdviceを使ったJava agentを作成する
では、Byte BuddyのAdviceを使った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.4</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.Agent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </plugin> </plugins> </build>
Maven Shade PluginでJava agent用にMETA-INF/MANIFEST.MFの設定を入れています。
@Advice.OnMethodEnterアノテーションおよび@Advice.OnMethodExitアノテーションを使ったクラスを
書いてみます。
src/main/java/org/littlewings/bytebuddy/MessageServiceLoggingInterceptor.java
package org.littlewings.bytebuddy; import java.lang.reflect.Method; import java.util.Arrays; import net.bytebuddy.asm.Advice; public class MessageServiceLoggingInterceptor { @Advice.OnMethodEnter public static void onMethodEnter( @Advice.Argument(0) String arg, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin ) { System.out.println("onMethodEnter:"); System.out.printf(" arg: %s%n", arg); System.out.printf(" args: %s%n", Arrays.asList(allArguments)); System.out.printf(" c: %s%n", c); System.out.printf(" thisInstance: %s%n", thisInstance); System.out.printf(" origin: %s%n", origin); } @Advice.OnMethodExit public static void onMethodExit( @Advice.Argument(0) String arg, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin, @Advice.Return Object returnValue ) { System.out.println("onMethodExit:"); System.out.printf(" arg: %s%n", arg); System.out.printf(" args: %s%n", Arrays.asList(allArguments)); System.out.printf(" c: %s%n", c); System.out.printf(" thisInstance: %s%n", thisInstance); System.out.printf(" origin: %s%n", origin); System.out.printf(" returnValue: %s%n", returnValue); } }
このクラスは、こちらのdecorateメソッドの呼び出し前後にロギングを追加します。
public class MessageService { private String c; public MessageService(String c) { this.c = c; } public String decorate(String message) { System.out.println("===== call MessageService#decorate ====="); return "%s%s%s %s %s%s%s".formatted(c, c, c, message, c, c, c); } }
メソッドの引数についているアノテーションを見ると、およそ意味はわかりそうな気はしますね。
@Advice.OnMethodEnter public static void onMethodEnter( @Advice.Argument(0) String arg, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin ) {
ポイントは、@Advice.OnMethodEnterアノテーションおよび@Advice.OnMethodExitアノテーションを
付与するメソッドはstaticメソッドである必要があります。
インスタンスメソッドに付与してしまうと、実行しても無視されます。特に例外がスローされたりせず、
単に無視されるだけなのでとてもわかりにくいです…。
@Advice.Argumentアノテーションの引数は、メソッドの何番目の引数なのかを表します。0から開始しますが、
この指定を誤るとこちらも無視されることになります…。
1度ここで動かしてみましょうか。
Java agent用のクラスを作成。
src/main/java/org/littlewings/bytebuddy/Agent.java
package org.littlewings.bytebuddy; import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; public class Agent { public static void premain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.named("org.littlewings.MessageService")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.visit(Advice.to(MessageServiceLoggingInterceptor.class).on(ElementMatchers.named("decorate"))) ) .installOn(inst); } }
Advice自体の使い方はとても簡単ですね。
builder.visit(Advice.to(MessageServiceLoggingInterceptor.class).on(ElementMatchers.named("decorate")))
Java agentをビルド。
$ mvn package
実行。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar -javaagent:/path/to/target/advice-agent-1.0-SNAPSHOT.jar -Dnet.bytebuddy.safe=true org.littlewings.App hello onMethodEnter: arg: hello args: [hello] c: ★ thisInstance: org.littlewings.MessageService@5f20155b origin: public java.lang.String org.littlewings.MessageService.decorate(java.lang.String) ===== call MessageService#decorate ===== onMethodExit: arg: hello args: [hello] c: ★ thisInstance: org.littlewings.MessageService@5f20155b origin: public java.lang.String org.littlewings.MessageService.decorate(java.lang.String) returnValue: ★★★ hello ★★★ message = ★★★ hello ★★★
Java agentを使わない場合はこうでしたね。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App hello ===== call MessageService#decorate ===== message = ★★★ hello ★★★
次は引数が2つあるメソッドに対する処理にしてみます。
src/main/java/org/littlewings/bytebuddy/MessageService2LoggingInterceptor.java
package org.littlewings.bytebuddy; import java.lang.reflect.Method; import java.util.Arrays; import net.bytebuddy.asm.Advice; public class MessageService2LoggingInterceptor { @Advice.OnMethodEnter public static void onMethodEnter( @Advice.Argument(0) String arg1, @Advice.Argument(1) String arg2, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin ) { System.out.println("onMethodEnter:"); System.out.printf(" arg1: %s%n", arg1); System.out.printf(" arg2: %s%n", arg2); System.out.printf(" args: %s%n", Arrays.asList(allArguments)); System.out.printf(" c: %s%n", c); System.out.printf(" thisInstance: %s%n", thisInstance); System.out.printf(" origin: %s%n", origin); } @Advice.OnMethodExit(onThrowable = RuntimeException.class) public static void onMethodExit( @Advice.Argument(0) String arg1, @Advice.Argument(1) String arg2, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin, @Advice.Return Object returnValue, @Advice.Thrown Throwable throwable ) { System.out.println("onMethodExit:"); System.out.printf(" arg1: %s%n", arg1); System.out.printf(" arg2: %s%n", arg2); System.out.printf(" args: %s%n", Arrays.asList(allArguments)); System.out.printf(" c: %s%n", c); System.out.printf(" thisInstance: %s%n", thisInstance); System.out.printf(" origin: %s%n", origin); System.out.printf(" returnValue: %s%n", returnValue); System.out.printf(" throwable: %s%n", throwable); } }
@Advice.OnMethodEnter側。こちらは@Advice.Argumentが2つになりました。引数の位置は0、1での
指定ですね。
@Advice.OnMethodEnter public static void onMethodEnter( @Advice.Argument(0) String arg1, @Advice.Argument(1) String arg2, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin ) {
@Advice.OnMethodExit側。こちらのポイントはonThrowable属性です。
@Advice.OnMethodExit(onThrowable = RuntimeException.class) public static void onMethodExit( @Advice.Argument(0) String arg1, @Advice.Argument(1) String arg2, @Advice.AllArguments Object[] allArguments, @Advice.FieldValue("c") String c, @Advice.This Object thisInstance, @Advice.Origin Method origin, @Advice.Return Object returnValue, @Advice.Thrown Throwable throwable ) {
onThrowableを指定すると、@Advice.Thrownが使えるようになります。
onThrowableを指定しない状態で@Advice.Thrownを使うと、この処理そのものが無視されます…。
このクラスは、こちらのdecorateメソッドの呼び出し前後にロギングを追加します。
public class MessageService2 { private String c; public MessageService2(String c) { this.c = c; } public String decorate(String message1, String message2) { System.out.println("===== call MessageService2#decorate ====="); if ("throw".equalsIgnoreCase(message1) && "exception".equalsIgnoreCase(message2)) { throw new IllegalArgumentException("Oops!!"); } return "%s%s%s %s %s!! %s%s%s".formatted(c, c, c, message1, message2, c, c, c); } }
Java agent用のクラスに、今回のクラスを追加。
src/main/java/org/littlewings/bytebuddy/Agent.java
package org.littlewings.bytebuddy; import java.lang.instrument.Instrumentation; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; public class Agent { public static void premain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.named("org.littlewings.MessageService")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.visit(Advice.to(MessageServiceLoggingInterceptor.class).on(ElementMatchers.named("decorate"))) ) .installOn(inst); new AgentBuilder.Default() .type(ElementMatchers.named("org.littlewings.MessageService2")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.visit(Advice.to(MessageService2LoggingInterceptor.class).on(ElementMatchers.named("decorate"))) ) .installOn(inst); } }
適用対象のクラスが増える場合は、AgentBuilderからinstallOnまでの流れがその分だけ増えますね。
実行してみます。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar -javaagent:/path/to/target/advice-agent-1.0-SNAPSHOT.jar -Dnet.bytebuddy.safe=true org.littlewings.App Hello World onMethodEnter: arg1: Hello arg2: World args: [Hello, World] c: ※ thisInstance: org.littlewings.MessageService2@37911f88 origin: public java.lang.String org.littlewings.MessageService2.decorate(java.lang.String,java.lang.String) ===== call MessageService2#decorate ===== onMethodExit: arg1: Hello arg2: World args: [Hello, World] c: ※ thisInstance: org.littlewings.MessageService2@37911f88 origin: public java.lang.String org.littlewings.MessageService2.decorate(java.lang.String,java.lang.String) returnValue: ※※※ Hello World!! ※※※ throwable: null message = ※※※ Hello World!! ※※※
Java agentを適用しない場合はこうでした。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App Hello World ===== call MessageService2#decorate ===== message = ※※※ Hello World!! ※※※
例外をスローするケースを実行しましょう。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar -javaagent:/path/to/target/advice-agent-1.0-SNAPSHOT.jar -Dnet.bytebuddy.safe=true org.littlewings.App throw exception onMethodEnter: arg1: throw arg2: exception args: [throw, exception] c: ※ thisInstance: org.littlewings.MessageService2@37911f88 origin: public java.lang.String org.littlewings.MessageService2.decorate(java.lang.String,java.lang.String) ===== call MessageService2#decorate ===== onMethodExit: arg1: throw arg2: exception args: [throw, exception] c: ※ thisInstance: org.littlewings.MessageService2@37911f88 origin: public java.lang.String org.littlewings.MessageService2.decorate(java.lang.String,java.lang.String) returnValue: null throwable: java.lang.IllegalArgumentException: Oops!! Exception in thread "main" java.lang.IllegalArgumentException: Oops!! at org.littlewings.MessageService2.decorate(MessageService2.java:14) at org.littlewings.App.main(App.java:10)
Java agentを適用しない場合はこうでした。
$ java -cp target/sample-app-1.0-SNAPSHOT.jar org.littlewings.App throw exception ===== call MessageService2#decorate ===== Exception in thread "main" java.lang.IllegalArgumentException: Oops!! at org.littlewings.MessageService2.decorate(MessageService2.java:14) at org.littlewings.App.main(App.java:10)
なんとなく、使い方がわかった感じです。
おわりに
Byte BuddyのAdviceを使って、メソッドの呼び出し前後に処理を追加するJava agentを書いてみました。
最初にByte Buddyを扱い始めた時はちょっとハードルが高く感じましたが、ちょっとずつ進めていくと
だいぶ慣れてきた気がしますね。
Byte Buddyについて扱うにはいったんここで区切りですが、Java agenも含めて勉強になりました。