JProfiler帮助文档

注入探针 (Injected Probes)

与脚本探针类似,注入探针 (injected probe) 为选定方法定义拦截处理程序。然而,注入探针是在 JProfiler GUI 外部的 IDE 中开发的,并依赖于 JProfiler 提供的注入探针 API。该 API 基于宽松的 Apache License 2.0 版进行许可,便于分发相关构件。

开始使用注入探针的最佳方式是研究 JProfiler 安装目录中 api/samples/simple-injected-probe 下的示例。在该目录中执行 ../gradlew 即可对其进行编译和运行。Gradle 构建文件 build.gradle 包含有关该示例的更多信息。api/samples/advanced-injected-probe 中的示例展示了探针系统的更多功能,包括控制对象 (control object)。

开发环境

开发注入探针所需的探针 API 包含在具有以下 Maven 坐标的单一构件中

group: com.jprofiler
artifact: jprofiler-probe-injected
version: <JProfiler version>

其中与本手册对应的 JProfiler 版本为 16.3。

JAR、源码和 Javadoc 构件均已发布到 Maven Central 仓库中。您可以使用 Gradle 或 Maven 等构建工具将探针 API 添加到开发类路径 (classpath) 中,也可以使用 JProfiler 安装目录中的 JAR 文件

api/jprofiler-probe-injected.jar

。

com.jprofiler.api.probe.injected 包的概述是探索该 API 的良好起点。

探针结构

注入探针是一个使用 com.jprofiler.api.probe.injected.Probe 注解的类。该注解的属性公开了整个探针的配置选项。例如,如果您创建了大量无需单独检查的探针事件,events 属性允许您禁用探针事件视图 (view) 并减少开销。

@Probe(name = "Foo", description = "Shows foo server requests", events = false)
public class FooProbe {
    ...
}

在探针类中,您可以添加带有特殊注解的静态方法来定义拦截处理程序。PayloadInterception 注解创建有效负载 (payload),而 SplitInterception 注解拆分调用树 (call tree)。根据注解的不同,处理程序的返回值将被用作有效负载或拆分字符串。与脚本探针一样,如果返回 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 包中的注解进行标注,以便 profiling agent 知道必须将哪个对象或基本类型值传递给该方法。例如,使用 @Parameter(0) 注解处理程序方法的参数会注入被拦截方法的第一个参数。

被拦截方法的方法参数对于所有拦截类型都可用。如果指示 profiling agent 拦截方法的退出而不是进入,有效负载拦截可以使用 @ReturnValue 访问返回值,或者使用 @ExceptionValue 访问抛出的异常 (exception)。这是通过 PayloadInterception 注解的 invokeOn 属性完成的。

与脚本探针不同,如果将拦截注解的 reentrant 属性设置为 true,则在被拦截方法发生递归调用时可以调用注入探针处理程序。通过在处理程序方法中添加 ProbeContext 类型的参数,您可以通过调用 ProbeContext.getOuterPayload() 或 ProbeContext.restartTiming() 来控制探针对嵌套调用的行为。

高级拦截

有时仅靠单次拦截不足以收集构建探针字符串所需的全部信息。为此,您的探针可以包含任意数量由 Interception 注解的拦截处理程序,这些处理程序不会创建有效负载或拆分。信息可以存储在探针类的静态字段中。为了在多线程环境中确保线程安全,您应该使用 ThreadLocal 实例来存储引用类型,并使用 java.util.concurrent.atomic 包中的原子数值类型来维护计数器。

在某些情况下,您需要同时针对方法进入和方法退出进行拦截。常见的情况是维护像 inMethodCall 这样的状态变量,用于修改另一个拦截的行为。您可以在进入拦截(这是默认的拦截类型)中将 inMethodCall 设置为 true。现在,在该拦截正下方定义另一个静态方法,并使用 @AdditionalInterception(invokeOn = InvocationType.EXIT) 进行注解。被拦截的方法取自上方的拦截处理程序,因此您无需再次指定。在方法体中,您可以将 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,否则控制对象视图 (view) 不可见。要使用控制对象,您必须在处理程序方法中声明一个 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 注解的静态方法来解析控制对象的显示名称。

如果处理程序方法返回 Payload 实例而不是 String,探针事件就可以与控制对象关联。ProbeContext.createPayload() 方法接收一个控制对象和一个探针事件类型。包含允许事件类型的枚举必须通过 Probe 注解的 customTypes 属性进行注册。

控制对象必须在时间测量的开始处指定,这对应于方法进入。在某些情况下,有效负载字符串的名称只有在方法退出时才可用,因为它取决于返回值或其他拦截。在这种情况下,您可以使用 ProbeContext.createPayloadWithDeferredName() 创建一个没有名称的有效负载对象。在正下方定义一个带有 @AdditionalInterception(invokeOn = InvocationType.EXIT) 注解的拦截处理程序,并从该方法返回一个 String,然后它将自动用作有效负载字符串。

覆盖线程状态

在测量数据库驱动程序或外部资源的本地连接器的执行时间时,有时需要告知 JProfiler 将某些方法置于不同的线程状态。例如,将数据库调用置于 "Net I/O" 线程状态是很有用的。如果通信机制没有使用标准 Java I/O 功能,而是使用某种本地机制,则不会自动处于该状态。

通过成对调用 ThreadState.NETIO.enter() 和 ThreadState.exit(),profiling agent 会相应地调整线程状态。

...

@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();
}

...

部署

部署注入探针有两种方式,取决于您是否希望将它们放在类路径 (classpath) 上。使用 VM 参数

-Djprofiler.probeClassPath=...

profiling agent 将设置一个单独的探针类路径。探针类路径可以包含目录和 JAR 文件,在 Windows 上以“;”分隔,在其他平台上以“:”分隔。profiling agent 将扫描该探针类路径并查找所有探针定义。

如果将探针类直接放置在类路径 (classpath) 上对您来说更方便,您可以将 VM 参数

-Djprofiler.customProbes=...
设置为以逗号分隔的完全限定类名列表。对于每个类名,profiling agent 都会尝试加载一个注入探针。