JProfiler帮助文档

注入式探针 (Injected Probes)

与脚本探针类似,注入式探针为选定的方法定义拦截处理器。 不同的是,注入式探针是在你的 IDE 中、JProfiler GUI 之外开发的,并依赖于 JProfiler 提供的注入式探针 API。 该 API 采用宽松的 Apache License 2.0 许可,便于分发相关的构件。

开始使用注入式探针的最佳方式是学习你 JProfiler 安装目录下 api/samples/simple-injected-probe 目录中的示例。 在该目录下执行 ../gradlew 以编译并运行示例。Gradle 构建文件 build.gradle 中包含了关于该示例的更多信息。 api/samples/advanced-injected-probe 目录下的示例展示了探针系统的更多特性,包括控制对象 (control object)。

开发环境

开发注入式探针所需的 probe API 包含在一个具有 Maven 坐标的单一构件中:

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

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

Jar、源码和 javadoc 构件已发布到 Maven Central 仓库。 你可以通过 Gradle 或 Maven 等构建工具将 probe API 添加到你的开发类路径 (classpath), 或者直接使用 JProfiler 安装目录下的 JAR 文件

api/jprofiler-probe-injected.jar 

进行开发。

概览 (overview) 文档是探索 com.jprofiler.api.probe. 包 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_splitting)。 处理器的返回值会根据注解类型被用作有效负载或拆分字符串。 与脚本探针类似,如果返回 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 允许类名为空,这样所有具有特定签名的方法都会被拦截,无论类名为何。 另外,你也可以使用 MethodSpecsubtypes 属性来拦截整个类层次结构。

与脚本探针自动提供所有参数不同,注入式探针的处理器方法需要声明参数以获取所需的值。 每个参数都需要使用 com.jprofiler.api.probe.injected.parameter 包中的注解进行标注,这样分析代理 (profiling agent) 才能知道应该传递哪个对象或基本类型值。 例如,处理器方法的参数使用 @Parameter(0) 注解时,会注入被拦截方法的第一个参数。

被拦截方法的参数在所有拦截类型中都可用。有效负载拦截 (payload interception) 可以通过 @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 中看到该用例的实际示例。

控制对象 (control object)

只有当 Probe 注解的 controlObjects 属性设置为 true 时,控制对象视图 (view) 才会显示。 若要操作控制对象,你需要在处理器方法中声明 ProbeContext 类型的参数以获取 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(),分析代理会相应调整线程状态。

... 

@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=... 

分析代理会设置一个独立的 probe 类路径 (probe classpath)。该 probe classpath 可以包含目录和 class 文件, 在 Windows 下用 ';' 分隔,在其他平台用 ':' 分隔。分析代理会扫描 probe classpath 并查找所有探针定义。

如果你更容易将探针类直接放在类路径 (classpath) 上,可以设置 VM 参数

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