これは、なにをしたくて書いたもの?
今までJava agentを利用したライブラリーなどは使ったことがありましたが、Java agentとして使うもの自体を
自分で書いたことがなかったので試してみようかなと。
Java agent
Java agentは、たまにjavaコマンドに-javaagentとして指定されるもののことです。
Java 25のドキュメントではこちら。
-javaagent:jarpath[=options] Loads the specified Java programming language agent. See java.lang.instrument.
日本語ドキュメントだとJava 21を載せておきます。
ここではJavaプログラミング言語エージェントとだけ書かれていて、java.lang.instrumentパッケージを見ることと
書かれています。
ではjava.lang.instrumentパッケージのJavadocを見てみましょう。
こちらも英語では25、日本語では21を載せています。
java.lang.instrument (Java SE 25 & JDK 25)
java.lang.instrument (Java SE 21 & JDK 21)
Javaプログラミング言語エージェント(以後Java agentと書きます)は、Java VM上で実行中のプログラムを
instrumentできる仕組みです。instrumentのメカニズムはメソッドのバイトコードを変更することです。
Java agentは以下の形態で提供されます。
- 実行可能JARファイル内のアプリケーションと一緒にパッケージングされる
- Java agent個別のJARファイルとしてパッケージングされる
よく見るのは後者だと思います。
Java agentは、JARファイル内に含まれるMETA-INF/MANIFEST.MFの属性により、JARファイル内のクラスの
ひとつがエージェントクラスとして識別されます。このエージェントクラスにはJava VMがJava agentを
起動するための特別なメソッドを定義する必要があります。
Java agentでは、主に以下のことを行うように実装されます。
- ロード時にクラスを任意の方法で変換する
- モジュールを変換する
- ロード済みのクラスのバイトコードを変換する
よって、Java agentを使う場合は信頼できるものを選ぶ必要があります。
Java agentの起動方法は、実行形態によって次の3つがあるようです。
起動方法によって、定義すべきメソッドや渡される引数の内容が変わるようです。
この中身を見ていく前に、先の内容を読んだ方がよさそうなので進めていきましょう。
Java agentのJARファイルからロードされたクラスは、
システムクラスローダー(ClassLoader#getSystemClassLoader)によってロードされ、無名モジュールの
メンバーとなります。エージェントクラスから参照できるのは以下のクラスです。
- ブートレイヤー内のモジュールによってエクスポートされるパッケージ内のクラス
- システムクラスローダー(通常はクラスパス)によって、無名モジュールのメンバーとして定義されるクラス
- Java agentがブートストラップクラスローダーによって定義され、無名モジュールのメンバーとして定義されるようにしたクラス
パッケージjava.lang.instrument / エージェント・クラスで利用可能なエージェント・クラスとモジュール/クラスの読み込み
JARファイルに含まれるMETA-INF/MANIFEST.MFのメインセクションのうち、次の属性はJava agentに対する
定義になります。
- Launcher-Agent-Class
- 実行可能JARファイル内のアプリケーションと一緒にパッケージングされている場合に、エージェントクラスのクラス名を指定する
- アプリケーションのmainメソッドが呼び出される前に、agentmainメソッドが呼び出される
- Premain-Class
- Agent-Class
- Boot-Class-Path
- ブートストラップクラスローダーで検索されるパスのリストで、ディレクトリーまたはライブラリーを指定する
- クラスを検索するプラットフォーム固有のメカニズムが失敗すると、これらのパスがブートストラップクラスローダーにより検索される
- Can-Redefine-Classes
- trueを指定した場合、クラスを再定義する機能がこのエージェントに必要であることを示す
- true以外の値を指定した場合はfalseと扱われ、デフォルト値もfalse
- Can-Retransform-Classes
- trueを指定した場合、クラスを再変換する機能がこのエージェントに必要であることを示す
- true以外の値を指定した場合はfalseと扱われ、デフォルト値もfalse
- Can-Set-Native-Method-Prefix
- trueを指定した場合、ネイティブメソッドのprefixを設定する機能がこのエージェントに必要であることを表す
- true以外の値を指定した場合はfalseと扱われ、デフォルト値もfalse
- この属性はオプション
パッケージjava.lang.instrument / JARファイル・マニフェスト属性
Java agentのJARファイルには、Premain-Class属性とAgent-Class属性の両方が存在することがあります。
-javaagentオプションを使って起動する時はPremain-Class属性が使われ、Java VMの起動後にJava agentを
起動する場合はAgent-Class属性が使われます。起動方法によって使われない属性が出てきますが、それは
無視されます。
ここまで見た後に、Java agentの起動方法を見返してみましょう。
実行可能JARファイル内のアプリケーションとともにパッケージ化されたエージェント
パッケージjava.lang.instrument / 実行可能JARファイル内のアプリケーションとともにパッケージ化されたエージェントの起動
アプリケーションが実行可能JARファイルであり、かつこの中にJava agentが含まれている場合の起動方法です。
この起動方法かつMETA-INF/MANIFEST.MFのLauncher-Agent-Class属性が指定されている場合、
アプリケーションのmainメソッドを呼び出す前に以下の2つのメソッドを探索して呼び出します。
- public static void agentmain(String agentArgs, Instrumentation inst)
- public static void agentmain(String agentArgs)
優先順は上からです。
agentArgsは常に空です。instはエージェントがコードのinstrumentに使用できます。
呼び出されたagentmainメソッドが例外をスローした場合、Java VMはアプリケーションのmainメソッドを
呼び出す前に終了します。
コマンドライン・インタフェースからのエージェントの起動
パッケージjava.lang.instrument / コマンドライン・インタフェースからのエージェントの起動
javaコマンドに-javaagent:<jarpath>[=<options>]オプションを指定して起動する方法ですね。この起動方法の場合、
META-INF/MANIFEST.MFのPremain-Class属性に指定されたクラス名から対象のクラスをロードし、
次のメソッドを呼び出します。
- public static void premain(String agentArgs, Instrumentation inst)
- public static void premain(String agentArgs)
優先順は上からです。
agentArgsにはoptionsで指定された値が渡されますが、単一の文字列として渡されるのでエージェントクラスが
パースする必要があります。instはエージェントがコードのinstrumentに使用できます。
エージェントクラスをロードできない、エージェントクラスが条件に一致するpremainメソッドを定義していない、
premainメソッドが例外をスローした場合はJava VMはアプリケーションのmainメソッドを呼び出す前に終了します。
なお、-javaagentオプションは複数回指定でき、複数のJava agentを呼び出すことができます。この場合、
各エージェントクラスのpremainメソッドはオプションで指定した順番で呼び出されます。
実行中のJVMでのエージェントの起動
パッケージjava.lang.instrument / 実行中のJVMでのエージェントの起動
アプリケーション内にJava agentが含まれていて、アプリケーションの実行中にJava agentを起動する方法です。
この起動方法の場合、Java agentは個別のJARファイルにパッケージングされている必要があります。
META-INF/MANIFEST.MFのAgent-Class属性に指定されたクラス名から対象のクラスをロードし、次のメソッドを
呼び出します。
- public static void agentmain(String agentArgs, Instrumentation inst)
- public static void agentmain(String agentArgs)
優先順は上からです。
agentArgsにはoptionsで指定された値が渡されますが、単一の文字列として渡されるのでエージェントクラスが
パースする必要があります。instはエージェントがコードのinstrumentに使用できます。
Agent-Class属性が定義されていなかったり、対象のクラスが見つからなかったりするとJava VMは異常終了しますが、
agentmainが例外をスローした場合は無視されます。
なお、この機能を使う場合はJava VMの起動時に-XX:+EnableDynamicAgentLoadingオプションを付けておくと
警告が抑制されます。
警告というのは、こういう内容ですね。
WARNING: A Java agent has been loaded dynamically ([JARファイルへのパス]) WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information WARNING: Dynamic loading of agents will be disallowed by default in a future release
警告内容を見るとわかりますが、将来的には動的なJava agentのロードはデフォルトでは無効になるようなので、
警告抑制のためというよりは機能を使う場合には-XX:+EnableDynamicAgentLoadingオプションを付けて
おいた方がよいでしょう。
では、ドキュメントを見るのはこれくらいにして少し試してみましょう。
今回は-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の前に、-javaagentを使って起動するアプリケーションが必要になります。
こんなソースコードを用意。
src/main/java/org/littlewings/SimpleHttpServer.java
package org.littlewings; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; public class SimpleHttpServer { private String address; private int port; public SimpleHttpServer(String address, int port) { this.address = address; this.port = port; } static void main() { SimpleHttpServer server = new SimpleHttpServer("0.0.0.0", 8080); server.start(); } public void start() { try { System.out.printf("[%s] simple http server[%s:%d] start.%n", LocalDateTime.now(), address, port); HttpServer httpServer = HttpServer.create(new InetSocketAddress(address, port), 0); httpServer.createContext("/", exchange -> { String message = "Hello World!!"; exchange.sendResponseHeaders(200, message.getBytes(StandardCharsets.UTF_8).length); try (OutputStream os = exchange.getResponseBody()) { os.write(message.getBytes(StandardCharsets.UTF_8)); } }); httpServer.setExecutor(null); httpServer.start(); } catch (IOException e) { throw new UncheckedIOException(e); } } }
そのまま実行してもいいのですが、Maven Shade PluginでJARを作成することにします。
<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> <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"> <mainClass>org.littlewings.SimpleHttpServer</mainClass> </transformer> </transformers> </configuration> </plugin> </plugins> </build>
パッケージングして実行。
$ mvn clean package $ java -jar target/simple-http-server-0.0.1-SNAPSHOT.jar [2026-01-01T22:33:59.011886495] simple http server[0.0.0.0:8080] start.
動作確認。
$ curl localhost:8080 Hello World!!
シンプルなJava agentを作成する
では、Java agentを作成してみます。Hello World的なシンプルなものにしましょう。
src/main/java/org/littlewings/SimpleAgent.java
package org.littlewings; import java.lang.instrument.Instrumentation; public class SimpleAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.printf("agent-args: %s%n", agentArgs); System.out.println("Hello Simple Java agent."); } }
agentArgsの内容とメッセージを出力しておしまいですね。
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> <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.SimpleAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </plugin> </plugins> </build>
ポイントはこちらですね。
<Premain-Class>org.littlewings.SimpleAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes>
今回はクラスの再定義も変換も行いませんが、設定しています。
パッケージング。
$ mvn clean package
META-INF/MANIFEST.MFの内容を確認してみましょう。
$ unzip -p target/simple-agent-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MF Manifest-Version: 1.0 Created-By: Maven JAR Plugin 3.4.1 Build-Jdk-Spec: 25 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: org.littlewings.SimpleAgent
よさそうです。
では、このJava agentを使ってみます。
$ java -javaagent:/path/to/simple-agent-0.0.1-SNAPSHOT.jar -jar target/simple-http-server-0.0.1-SNAPSHOT.jar agent-args: null Hello Simple Java agent. [2026-01-01T22:39:09.618363733] simple http server[0.0.0.0:8080] start.
動いていますね、よさそうです。
オプションも付けてみましょう。
$ java -javaagent:/path/to/simple-agent-0.0.1-SNAPSHOT.jar=foo=bar,hoge=fuga -jar target/simple-http-server-0.0.1-SNAPSHOT.jar agent-args: foo=bar,hoge=fuga Hello Simple Java agent. [2026-01-01T22:40:07.976626041] simple http server[0.0.0.0:8080] start.
agentArgsに「=」以降の値がそのまま渡されていることが確認できました。
依存ライブラリーを含むJava agentを作成する
最後に依存ライブラリーを含むJava agentを作成してみます。
SLF4Jを使うようにしましょう。
src/main/java/org/littlewings/MyAgent.java
package org.littlewings; import java.lang.instrument.Instrumentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { Logger logger = LoggerFactory.getLogger(MyAgent.class); logger.info("agent-args: {}", agentArgs); logger.info("Hello My 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>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.17</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.17</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.MyAgent</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を依存ライブラリーを含めてまとめたJARファイルにする目的でも使います。
パッケージングして
$ mvn clean package
実行。
$ java -javaagent:/path/to/my-agent-0.0.1-SNAPSHOT.jar -jar target/simple-http-server-0.0.1-SNAPSHOT.jar
問題なく実行できました。
[main] INFO org.littlewings.MyAgent - agent-args: null [main] INFO org.littlewings.MyAgent - Hello My Java agent. [2026-01-01T23:47:14.275668433] simple http server[0.0.0.0:8080] start.
ちょっとした注意点として、署名を含むJARファイルの場合はこうやって複数のJARファイルをまとめてしまうと
署名と合わなくなりJava VMの実行に失敗するようになります。
Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes at java.base/sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:339) at java.base/sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:281) at java.base/java.util.jar.JarVerifier.processEntry(JarVerifier.java:323) at java.base/java.util.jar.JarVerifier.update(JarVerifier.java:235) at java.base/java.util.jar.JarFile.initializeVerifier(JarFile.java:739) at java.base/java.util.jar.JarFile.ensureInitialization(JarFile.java:1049) at java.base/java.util.jar.JavaUtilJarAccessImpl.ensureInitialization(JavaUtilJarAccessImpl.java:42) at java.base/jdk.internal.loader.URLClassPath$JarLoader$1.getManifest(URLClassPath.java:720) at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:762) at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:691) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:620) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:578) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:488) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:556) *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message Outstanding error when calling method in invokeJavaAgentMainMethod at ./src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 627 *** java.lang.instrument ASSERTION FAILED ***: "success" with message invokeJavaAgentMainMethod failed at ./src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 466 *** java.lang.instrument ASSERTION FAILED ***: "result" with message agent load/premain call failed at ./src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 429 FATAL ERROR in native method: processing of -javaagent failed, processJavaStart failed Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) V [libjvm.so+0x9ffc18] JavaThread::print_jni_stack()+0xf8 V [libjvm.so+0xa91d39] jni_FatalError+0xa9 V [libjvm.so+0xc0d984] JvmtiExport::post_vm_initialized()+0x394 V [libjvm.so+0x11938a3] Threads::create_vm(JavaVMInitArgs*, bool*)+0x923 V [libjvm.so+0xa9f4c3] JNI_CreateJavaVM+0x53 C [libjli.so+0x4090] JavaMain+0xa0 C [libjli.so+0x82bd] ThreadJavaMain+0xd C [libc.so.6+0x9caa4] 中止 (コアダンプ)
この場合は、以下のように署名に関するファイルを除外する設定を入れるとよいでしょう。
<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.MyAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </plugin>
この部分ですね。
<filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters>
これはJava agentというより、Maven Shade Pluginの話のような気もしますが。
またアプリケーション側の依存ライブラリーと競合する可能性がある場合は、relocationを使うのもよさそうです。
Relocating Classes – Apache Maven Shade Plugin
おわりに
Java agentを試してみました。
今まで使うだけでどうやって作るのかをちゃんと見たことがなかったので、学ぶ良い機会になりました。
実際にクラスの再定義などはByte Buddyを使ってやってみたいと思います。