スクリプトプローブと同様に、インジェクトプローブは選択したメソッドに対するインターセプションハンドラを定義します。 ただし、インジェクトプローブはJProfilerのGUI外、つまりご自身のIDEで開発され、JProfilerが提供するインジェクトプローブAPIに依存します。 このAPIはパーミッシブなApache License, version 2.0の下でライセンスされており、関連するアーティファクトの配布が容易です。
インジェクトプローブを始める最良の方法は、JProfilerインストールディレクトリ内の
api/samples/simple-injected-probe ディレクトリにあるサンプルを参照することです。
そのディレクトリで ../gradlew を実行して、サンプルをコンパイル・実行できます。Gradleの
ビルドファイル build.gradle にはサンプルに関する追加情報が記載されています。
api/samples/advanced-injected-probe のサンプルでは、制御オブジェクトなど、プローブシステムのより多くの機能が紹介されています。
開発環境
インジェクトプローブを開発するために必要なプローブAPIは、以下のMaven座標を持つ単一のアーティファクトに含まれています。
group: com.jprofiler artifact: jprofiler-probe-injected version: <JProfiler version>
このマニュアルに対応するJProfilerのバージョンは 16.0 です。
Jar、ソース、およびjavadocアーティファクトはMaven Centralリポジトリに公開されています。 GradleやMavenなどのビルドツールでプローブAPIを開発用クラスパスに追加するか、 JARファイル
api/jprofiler-probe-injected.jar
をJProfilerインストールディレクトリから利用することもできます。
概要は、
com.jprofiler.api.probe. パッケージのAPIを探求する際の良い出発点です。
プローブの構造
インジェクトプローブは com.jprofiler.api.probe.injected.Probe でアノテートされたクラスです。
このアノテーションの属性でプローブ全体の設定オプションを指定できます。
例えば、個別に調査する必要のない多数のプローブイベントを生成する場合、events 属性でプローブイベントビューを無効化し、オーバーヘッドを削減できます。
@Probe(name = "Foo", description = "Shows foo server requests", events = "false")
public class FooProbe {
...
}
プローブクラスには、インターセプションハンドラを定義するために特別なアノテーション付きのstaticメソッドを追加します。
PayloadInterception アノテーションはペイロードを生成し、SplitInterception
アノテーションは呼び出しツリーの分割を行います。ハンドラの戻り値は、アノテーションに応じてペイロードまたは分割文字列として使用されます。
スクリプトプローブと同様に、null を返すとインターセプションは効果を持ちません。インターセプトされたメソッドのタイミング情報は自動的に計算されます。
@Probe(name = "FooBar")
public class FooProbe {
@PayloadInterception(
invokeOn = InvocationType.ENTER,
method = @MethodSpec(className = "com.bar.Database",
methodName = "processQuery",
parameterTypes = {"com.bar.Query"},
returnType = "void"))
public static String fooRequest(@Parameter(0) Query query) {
return query.getVerbose();
}
@SplitInterception(
method = @MethodSpec(className = "com.foo.Server",
methodName = "handleRequest",
parameterTypes = {"com.foo.Request"},
returnType = "void"))
public static String barQuery(@Parameter(0) Request request) {
return request.getPath();
}
}
上記の例のように、両方のアノテーションには method 属性があり、
MethodSpec でインターセプトするメソッドを定義します。
スクリプトプローブと異なり、MethodSpec ではクラス名を空にでき、
その場合、特定のシグネチャを持つすべてのメソッドがクラス名に関係なくインターセプトされます。
また、MethodSpec の subtypes 属性を使って、クラス階層全体をインターセプトすることも可能です。
スクリプトプローブではすべてのパラメータが自動的に利用可能ですが、インジェクトプローブのハンドラメソッドでは必要な値を取得するためのパラメータを宣言します。
各パラメータには com.jprofiler.api.probe.injected.parameter パッケージのアノテーションを付与し、
プロファイリングエージェントがどのオブジェクトやプリミティブ値をメソッドに渡すべきかを認識できるようにします。
例えば、ハンドラメソッドのパラメータに @Parameter(0) を付与すると、インターセプトされたメソッドの最初のパラメータが注入されます。
インターセプトされたメソッドのパラメータは、すべてのインターセプションタイプで利用可能です。
ペイロードインターセプションでは、@ReturnValue で戻り値、@ExceptionValue でスローされた例外にアクセスできます。
これらは、メソッドのエントリではなく終了時にインターセプトするようプロファイリングエージェントに指示した場合に利用できます。
これは PayloadInterception アノテーションの invokeOn 属性で指定します。
スクリプトプローブと異なり、インジェクトプローブのハンドラは、インターセプトされたメソッドの再帰呼び出し時にも呼ばれる場合があります。
これにはインターセプションアノテーションの reentrant 属性を true に設定します。
また、ハンドラメソッドのパラメータに ProbeContext 型を追加することで、
ProbeContext.getOuterPayload() や ProbeContext.restartTiming() を呼び出して、
ネストされた呼び出しに対するプローブの挙動を制御できます。
高度なインターセプション
プローブ文字列を構築するために必要な情報を収集するには、単一のインターセプションだけでは不十分な場合があります。
そのため、ペイロードや分割を生成しない Interception アノテーション付きのインターセプションハンドラを任意の数だけプローブに含めることができます。
情報はプローブクラスのstaticフィールドに保存できます。
マルチスレッド環境でのスレッドセーフのため、参照型の保存には ThreadLocal インスタンス、カウンタの管理には
java.util.concurrent.atomic パッケージのアトミック数値型を使用してください。
状況によっては、メソッドのエントリと終了の両方でインターセプションが必要になる場合があります。
よくあるケースとして、inMethodCall のような状態変数を管理し、他のインターセプションの挙動を変更する場合です。
エントリインターセプション(デフォルト)で inMethodCall を true に設定し、
その直下に @AdditionalInterception(invokeOn = InvocationType.EXIT) でアノテートしたstaticメソッドを定義します。
インターセプションハンドラの上で指定したメソッドが対象となるため、再度指定する必要はありません。
メソッド本体で inMethodCall 変数を false に設定できます。
...
private static final ThreadLocal<Boolean> inMethodCall =
ThreadLocal.withInitial(() -> Boolean.FALSE);
@Interception(
invokeOn = InvocationType.ENTER,
method = @MethodSpec(className = "com.foo.Server",
methodName = "internalCall",
parameterTypes = {"com.foo.Request"},
returnType = "void"))
public static void guardEnter() {
inMethodCall.set(Boolean.TRUE);
}
@AdditionalInterception(InvocationType.EXIT)
public static void guardExit() {
inMethodCall.set(Boolean.FALSE);
}
@SplitInterception(
method = @MethodSpec(className = "com.foo.Server",
methodName = "handleRequest",
parameterTypes = {"com.foo.Request"},
returnType = "void"),
reentrant = true)
public static String splitRequest(@Parameter(0) Request request) {
if (!inMethodCall.get()) {
return request.getPath();
} else {
return null;
}
}
...
このユースケースの動作例は
api/samples/advanced-injected-probe/src/main/java/AdvancedAwtEventProbe.java
で確認できます。
制御オブジェクト
制御オブジェクトビューは、Probe アノテーションの controlObjects 属性を true に設定しない限り表示されません。
制御オブジェクトを操作するには、ハンドラメソッドのパラメータとして ProbeContext 型を宣言して取得する必要があります。
以下のサンプルコードは、制御オブジェクトをオープンし、プローブイベントと関連付ける方法を示しています。
@Probe(name = "Foo", controlObjects = true, customTypes = MyEventTypes.class)
public class FooProbe {
@Interception(
invokeOn = InvocationType.EXIT,
method = @MethodSpec(className = "com.foo.ConnectionPool",
methodName = "createConnection",
parameterTypes = {},
returnType = "com.foo.Connection"))
public static void openConnection(ProbeContext pc, @ReturnValue Connection c) {
pc.openControlObject(c, c.getId());
}
@PayloadInterception(
invokeOn = InvocationType.EXIT,
method = @MethodSpec(className = "com.foo.ConnectionPool",
methodName = "createConnection",
parameterTypes = {"com.foo.Query", "com.foo.Connection"},
returnType = "com.foo.Connection"))
public static Payload handleQuery(
ProbeContext pc, @Parameter(0) Query query, @Parameter(1) Connection c) {
return pc.createPayload(query.getVerbose(), c, MyEventTypes.QUERY);
}
...
}
制御オブジェクトには明確なライフタイムがあり、プローブビューはそのオープン・クローズ時刻をタイムラインおよび制御オブジェクトビューに記録します。
可能であれば、ProbeContext.openControlObject() および ProbeContext.closeControlObject()
を呼び出して明示的に制御オブジェクトを開閉してください。
そうでない場合は、@ControlObjectName でアノテートされたstaticメソッドを宣言し、制御オブジェクトの表示名を解決する必要があります。
ハンドラメソッドが String ではなく Payload のインスタンスを返す場合、プローブイベントを制御オブジェクトと関連付けることができます。
ProbeContext.createPayload() メソッドは制御オブジェクトとプローブイベントタイプを受け取ります。
許可されるイベントタイプのenumは、Probe アノテーションの customTypes 属性で登録する必要があります。
制御オブジェクトは、時間計測の開始時(メソッドエントリ)に指定する必要があります。
場合によっては、ペイロード文字列の名前が戻り値や他のインターセプションに依存し、メソッド終了時にしか判明しないことがあります。
その場合は ProbeContext.createPayloadWithDeferredName() を使って名前なしのペイロードオブジェクトを作成し、
直下に @AdditionalInterception(invokeOn = InvocationType.EXIT) でアノテートしたインターセプションハンドラを定義して、
そのメソッドから String を返すことで、自動的にペイロード文字列として利用されます。
スレッド状態の上書き
データベースドライバや外部リソースへのネイティブコネクタの実行時間を計測する際、一部のメソッドを異なるスレッド状態に設定する必要が生じる場合があります。 例えば、データベース呼び出しを「Net I/O」スレッド状態にするのが有用です。 通信機構が標準のJava I/O機能を使わず、ネイティブな仕組みを利用している場合は、自動的にその状態にはなりません。
ThreadState.NETIO.enter() と ThreadState.exit() のペアを呼び出すことで、
プロファイリングエージェントはスレッド状態を適切に調整します。
...
@Interception(invokeOn = InvocationType.ENTER, method = ...)
public static void enterMethod(ProbeContext probeContext, @ThisValue JComponent component) {
ThreadState.NETIO.enter();
}
@AdditionalInterception(InvocationType.EXIT)
public static void exitMethod() {
ThreadState.exit();
}
...
デプロイ
インジェクトプローブのデプロイ方法は2通りあり、クラスパスに配置するかどうかで異なります。 VMパラメータ
-Djprofiler.probeClassPath=...
を指定すると、プロファイリングエージェントによって専用のプローブクラスパスが設定されます。 プローブクラスパスにはディレクトリやクラスファイルを含めることができ、Windowsでは「;」、その他のプラットフォームでは「:」で区切ります。 プロファイリングエージェントはプローブクラスパスをスキャンし、すべてのプローブ定義を検出します。
プローブクラスをクラスパスに配置する方が簡単な場合は、VMパラメータ
-Djprofiler.customProbes=...を完全修飾クラス名のカンマ区切りリストに設定してください。各クラス名について、プロファイリングエージェントはインジェクトプローブのロードを試みます。