Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

JVMにおけるServiceLoaderとjavaagent

本エントリーはJava Advent Calendarシリーズ2の19日目です。

qiita.com

OpenTelemetryの自動計装機能を調べていたら、 ServiceLoader とjavaagentを活用した実装になっていました。これ前知識がないと魔法に見えるやつだなって思ったのですが、 ServiceLoader はともかくjavaagentなんてなかなか使わないだろうなぁと思ったので簡単な説明を書いてみます。

ServiceLoaderはJVM標準のDI技術

ServiceLoader とはJava 1.6から提供されている仕組みです。ので現代で使われているJVMでは必ず使える機能のひとつですね。

サービスとは、既知のインタフェースおよびクラス(通常は抽象クラス)のセットです。サービス・プロバイダとは、特定のサービスの実装です。通常、プロバイダのクラスによって、サービス自体に定義されているクラスのインタフェースとサブクラスが実装されます。 https://docs.oracle.com/javase/jp/8/docs/api/java/util/ServiceLoader.html

ServiceLoader を端的に説明すると、Javaプログラマに通りのいい表現としては、DIのようなものです。このインタフェースを実装してるクラスの実装がほしいよ〜、というコードを書いておくと、それをCLASSPATHから探して渡してくれるんです。

class よくあるDI {
  @Inject
  よくあるDI(Collection<適当なインタフェース> 引数) {
    // 指定したインタフェースの実装をコンストラクタに渡してくれる
  }
}
class ServiceLoader動作イメージ {
  void method() {
    ServiceLoader serviceLoader = ServiceLoader.load(適当なインタフェース.class)
    serviceLoader.iterator() // 指定したインタフェースの実装をイテレートするIteratorを返してくれる
  }
}

なお実装のインスタンスはデフォルトコンストラクタによって作成されます。このため複雑なインスタンスを作ることはできませんが、特定の処理を開始する起点としては充分なことが多いです。

例えばログファサードライブラリであるSLF4Jでは、この機能を使ってバインディングと呼ばれるインタフェースの実装を探しています。SLF4JはLog4j2やLogbackなどのロギングライブラリに処理を委譲しますが、このときに「Log4j2が使えるのか、Logbackが使えるのか?」を判断するのにService Loaderを使っているわけです。

OpenTelemetryでは InstrumentationModule の実装を探すのに ServiceLoader を使っています。何からデータを集めたいのか(RDB、gRPC、HTTPサーバなどなど)に応じたライブラリをCLASSPATHに入れておけば、起動時に ServiceLoader がよしなに必要なコードを拾い集めて初期化してくれる、そういうコードが簡単に書けるわけです。業務ロジック側では、それらのライブラリの存在すら認識する必要はありません。

ちなみに ServiceLoader でインスタンス化される側のコードを書く場合、ほぼセットで利用するのがGoogleのAuto Serviceです。Service Loaderでインスタンス化される側のコードを書く際に「Service Loaderに実装の居場所を教えるためのファイル」を作成する必要があるのですが、これをアノテーションから自動的に生成してくれます。便利ですね。

main関数以前の処理を実現するjavaagent

次にjavaagentの話をします。皆さんがJavaプログラマであれば、 public static void main(String[] args) な関数を一度は書いたことがあるでしょう。え? public static なんて今ドキ書かないって?2023年ですね。

ところでJVMはこの main 関数を実行する前、何をしているのでしょうか。メモリの確保とか、GCの準備とか、 ClassLoader の作成とか、プロセス引数から String[] を作るとか、色々やってそうですよね。そこで自分のコードを動かせたら最高じゃないですか?例えば ClassLoader 作成に一枚噛めるということは、これは事実上JVMが利用するほぼすべてのクラスのロードに介入して改変できることを意味しています。実際OpenTelemetryではメソッドの実装に介入してメソッド実行時に引数を差し替えるなんて芸当をしています。

実際こういう実装の差し替えとかやりはじめると、考慮することが結構あって大変なんですけどね。bytecode manipilationと呼ばれる分野で、筆者の好物のひとつでもあります。

さてjavaagentの話に戻ります。javaagentは java.lang.instrument パッケージに説明があります。javaagentにはそのままズバリの premain という関数を実装すればよさそうです。なるほど、 main 関数の前に呼び出されそうな名前ですね。さらにクラス定義を差し替えるためのメソッドなども提供されていて、いかにもJVMをハックするためのAPIという感じです。

のでひとつ注意点があって、javaagentは使うとどうしてもJVM起動速度にペナルティがかかります。迅速に起動を終わらせてリクエストを受け付けられるようにしたいんだという場合、javaagentの利用を避けて手で各種初期化を行うことも検討せざるを得ない……ということもあるでしょう。

以上でざっくりした説明を終わります。OpenTelemetryは ServiceLoader とjavaagentの合わせ技で、javaagentのCLASSPATHに入っているインタフェースの実装を main 関数実行前に使うことで、 ClassLoader に手を加えて計測対象モジュールに対して自動的に初期化を行っていました。一見便利ですがjavaagentの持つ速度的なペナルティや、javaagentのCLASSPATHが大きくなりがちという問題も潜んでいます。ご利用は計画的に。