以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2026/01/17/223042より取得しました。


Byte BuddyのAdviceを使って、メソッドの呼び出し前後に処理を追加するJava agentを書いてみる

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

最近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に書かれているのですが、以下のようなアノテーションが使えます。

参考)

My daily Java: Using Byte Buddy for proxy creation

https://github.com/raphw/byte-buddy/tree/byte-buddy-1.18.4/byte-buddy-dep/src/test/java/net/bytebuddy/asm

ただ、説明を見てもわからない気もするので実際に使ってみるとしましょう。

今回はJava agentとして作ります。

Java agentとByte Buddyの使い方については以前のエントリーを参照してください。

Java agentを書いてみる - CLOVER🍀

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も含めて勉強になりました。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2026/01/17/223042より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14