原文はこちら。
The original article was written by Erik Gahlin (Consulting Member of Technical Staff, Java Platform Group, Oracle).
https://egahlin.github.io/2024/05/31/deprecated-event.html
JDK 22で廃止予定のメソッド呼び出しを検出するためにあるイベントが追加されました。主要用途は、3rdパーティーライブラリが削除予定のメソッド(例えばSecurity Managerに関連するメソッド)に依存しているかどうかを判断するためです。Security Managerに関する詳細情報は、以下のJEPをご覧ください。
JEP 411: Deprecate the Security Manager for Removal
https://openjdk.org/jeps/411
廃止予定のメソッドの利用を開発プロセス早期の段階で検出することで、アップグレードや別のライブラリへの切り替え、もしくはライブラリメンテナーとともにバグ登録する、といったことに時間をとることができます。将来的には、JDK Mission Controlを拡張し、廃止予定メソッドの呼び出しを検知するためのルールが追加されるかもしれません。
JMC 9.0.0 General-Availability Release
https://jdk.java.net/jmc/9/
このイベントがどのように機能するかを説明するため、2個のクラスを使います。両方ともに削除予定のメソッド呼び出しが含まれています。
public class API {
public static void enableLogging(boolean enable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
System.setProperty("log", String.valueOf(enable));
return null;
}
});
}
public static void runTask(Runnable task) {
try {
task.run();
} catch (ThreadDeath td) {
System.out.println("Task stopped.");
}
}
}
public class Service {
public static void log(String message) {
String shouldLog = System.getProperty("log", "true");
if (new Boolean("log")) {
System.out.print(message);
}
}
}
上記クラスをコンパイルすると3個の警告メッセージが出ます。
$ javac API.java Service.java
API.java:7: warning: [removal] AccessController in java.security has been deprecated and marked for removal
AccessController.doPrivileged(new PrivilegedAction<Void>() {
^
API.java:18: warning: [removal] ThreadDeath in java.lang has been deprecated and marked for removal
} catch (ThreadDeath td) {
^
Service.java:4: warning: [removal] Boolean(String) in Boolean has been deprecated and marked for removal
if (new Boolean("log")) {
^
3 warnings
これらの警告メッセージを修正すべきですが、このクラスがライブラリに含まれるもので、このメソッドが廃止予定に到達する前にコンパイルされたものをダウンロードしたのだとすれば、このメッセージを見逃す可能性があります。これらのクラスをJARファイルに入れ、両クラスを使うApplicationクラスを作成し、JFRとともにアプリケーションを実行してみます。
$ jar cf library.jar *.class
$ rm *.class *.java
public class Application {
public static void main(String... args) throws Exception {
API.enableLogging(true);
Class.forName("Service").getMethod("log").invoke(null, "Program started.");
}
}
非推奨イベント(deprecated event)はデフォルトで有効なので、JFR起動時に余分な構成は不要です。
$ java -XX:StartFlightRecording:filename=recording.jfr -cp library.jar Application.java
上記の例では、アプリケーション終了時に記録ファイルを書き出します。jfr viewコマンドを使って呼び出しを確認できます。
$ jfr view deprecated-methods-for-removal recording.jfr
Deprecated Methods for Removal
Deprecated Method Called from Class
------------------------------------------------------------- -----------------
java.lang.Boolean.<init>(String) Service
java.security.AccessController.doPrivileged(PrivilegedAction) API
API::runTaskメソッドがリストアップされていないことに注目してください。これには以下の2個の理由があります。
API::runTaskメソッドが呼び出されていないため(JFRは実際に呼び出されたメソッドのみをレポートする)。- メソッドが廃止予定のクラスを参照しているものの、JFRは廃止予定のメソッド呼び出しを追跡するため(参照するだけではJFRは追跡できない)。
ライブラリ内での廃止予定APIのすべての利用状況を把握したい場合には、jdeprscanツールを使ってください。
$ jdeprscan --for-removal library.jar
Jar file library.jar:
class API uses deprecated class java/security/AccessController (forRemoval=true)
class API uses deprecated class java/lang/ThreadDeath (forRemoval=true)
class Service uses deprecated method java/lang/Boolean::<init>(Ljava/lang/String;)V (forRemoval=true)
JFRが発生したイベントには、呼び出し元と呼び出された側のイベントが含まれています。これはjfr printコマンドで確認できます。
$ jfr print --events jdk.DeprecatedInvocation recording.jfr
jdk.DeprecatedInvocation {
startTime = 00:31:21.837 (2024-06-02)
method = java.lang.Boolean.<init>(String)
invocationTime = 00:31:21.834 (2024-06-02)
forRemoval = true
stackTrace = [
Service.log(String) line: 4
...
]
}
jdk.DeprecatedInvocation {
startTime = 00:31:21.837 (2024-06-02)
method = java.security.AccessController.doPrivileged(PrivilegedAction)
invocationTime = 00:31:21.834 (2024-06-02)
forRemoval = true
stackTrace = [
API.enableLogging(boolean) line: 7
...
]
}
イベントの初期設計時には、stackTraceフィールドは存在せず、methodおよびcallerのフィールドのみがありました。callerをstackTraceフィールドの一番上のフレームに置くことで、既存の可視化ツールとよりうまく連携できるようになりました。イベントに対して複数のフレームを取得することはできません。
廃止予定メソッド(deprecated-methods-for-removal)ビューに、呼び出し元クラス(呼び出し元メソッドではない)がリストアップされるのは、JFR が JVM 内のメソッド解決に依拠しているためです。 インタープリタでは、呼び出し元クラスごとに 1 回だけチェックが行われます。 つまり、特定のクラスの特定のメソッドへの最初のコールサイトのみでイベントが生成されます。JIT コンパイルされている場合は、すべてのコールサイトが報告されます。 この制限は将来的に解除される可能性があります。
@Deprecated(forRemoval=false)が設定されているメソッド呼び出しを記録するためには、levelというイベント設定を使うことで実現できます。設定可能な値はforRemovalとallです。
$ java -XX:StartFlightRecording:jdk.DeprecatedInvocation#level=all,filename=recording.jfr -cp library.jar Application.java
このブログエントリが、非推奨イベントの機能と制限について、理解を深める助けになれば幸いです。より深い理解を得られたと思います。このイベントの最大の利点は、JVMを継続的に監視するシステムにおいて発揮されるでしょう。これにより、非推奨メソッドの使用も検出できるようになります。