JProfiler帮助文档

嵌入式探针 (Embedded Probes)

如果你能够控制目标软件组件的源代码,建议编写嵌入式探针 (Embedded Probe),而不是注入式探针 (Injected Probe)。

编写注入式探针时,大部分初始工作都集中在指定被拦截的方法,以及选择作为 handler 方法参数的注入对象。而使用嵌入式探针时,这些都不是必须的,因为你可以直接在被监控方法中调用嵌入式探针 API。嵌入式探针的另一个优势是部署自动化。探针会和你的软件一起打包,并在应用被分析 (profiled) 时出现在 JProfiler UI 中。你唯一需要随软件分发的依赖是一个小型 JAR 文件,该文件基于 Apache 2.0 License 授权,主要包含一些空方法体,作为 profiling agent 的 hook。

开发环境

开发环境与注入式探针相同,区别在于 artifact 名称为 jprofiler-probe-embedded,而不是 jprofiler-probe-injected,并且你需要将 JAR 文件与应用一起分发,而不是在单独项目中开发探针。你需要的嵌入式探针 API 已包含在这个单一的 JAR artifact 中。查阅 javadoc 时,建议从 包概览 com.jprofiler.api.probe.embedded 开始了解 API。

和注入式探针一样,嵌入式探针也有两个示例。在 api/samples/simple-embedded-probe 目录下有一个入门示例,帮助你开始编写嵌入式探针。在该目录下执行 ../gradlew 以编译并运行示例,并参考 gradle 构建文件 build.gradle 了解执行环境。更多特性(包括控制对象)可参考 api/samples/advanced-embedded-probe 目录下的高级示例。

有效负载探针 (Payload Probes)

与注入式探针类似,你仍然需要一个探针类用于配置。该探针类必须继承 com.jprofiler.api.probe.embedded.PayloadProbecom.jprofiler.api.probe.embedded.SplitProbe,具体取决于你的探针是收集有效负载 (payload) 还是进行调用树拆分 (call_tree_splitting)。在注入式探针 API 中,你会在 handler 方法上使用不同的注解来区分有效负载收集和拆分。嵌入式探针 API 则没有 handler 方法,需要将这些配置转移到探针类本身。

public class FooPayloadProbe extends PayloadProbe { 
    @Override 
    public String getName() { 
        return "Foo queries"; 
    } 

    @Override 
    public String getDescription() { 
        return "Records foo queries"; 
    } 
} 

注入式探针通过注解进行配置,而嵌入式探针则通过重写探针基类的方法进行配置。对于有效负载探针,唯一的抽象方法是 getName(),其他方法都有默认实现,你可以根据需要重写。例如,如果你想禁用事件视图 (events view) 以减少开销,可以重写 isEvents() 方法并返回 false

要收集有效负载并测量其相关耗时,你需要成对调用 Payload.enter()Payload.exit()

public void measuredCall(String query) { 
    Payload.enter(FooPayloadProbe.class); 
    try { 
        performWork(); 
    } finally { 
        Payload.exit(query); 
    } 
} 

Payload.enter() 调用以探针类作为参数,这样 profiling agent 就能知道调用目标是哪个探针,Payload.exit() 会自动关联到同一个探针,并以有效负载字符串作为参数。如果遗漏了 exit 调用,调用树 (call tree) 会被破坏,因此应始终在 try 的 finally 块中调用。

如果被测代码块没有返回值,可以直接调用 Payload.execute 方法,该方法接受有效负载字符串和一个 Runnable。在 Java 8+ 中,可以用 lambda 或方法引用让调用更简洁。

public void measuredCall(String query) { 
  Payload.execute(FooPayloadProbe.class, query, this::performWork); 
} 

这种情况下,有效负载字符串必须事先已知。execute 还有一个重载版本,接受 Callable

public QueryResult measuredCall(String query) throws Exception { 
    return Payload.execute(PayloadProbe.class, query, () -> query.execute()); 
} 

Callable 参数的方法的一个问题是 Callable.call() 会抛出受检 Exception,因此你必须捕获异常或在包含方法上声明抛出异常。

控制对象 (Control Objects)

有效负载探针可以通过调用 Payload 类中的相关方法来打开和关闭控制对象 (control_object)。通过将控制对象和自定义事件类型传递给带有这些参数的 Payload.enter()Payload.execute() 方法,可以将它们与探针事件关联起来。

public void measuredCall(String query, Connection connection) { 
    Payload.enter(FooPayloadProbe.class, connection, MyEventTypes.QUERY); 
    try { 
        performWork(); 
    } finally { 
        Payload.exit(query); 
    } 
} 

控制对象视图 (control object view) 需要在探针配置中显式启用,自定义事件类型也必须在探针类中注册。

public class FooPayloadProbe extends PayloadProbe { 
    @Override 
    public String getName() { 
        return "Foo queries"; 
    } 

    @Override 
    public String getDescription() { 
        return "Records foo queries"; 
    } 

    @Override 
    public boolean isControlObjects() { 
        return true; 
    } 

    @Override 
    public Class<? extends Enum> getCustomTypes() { 
        return Connection.class; 
    } 
} 

如果你没有显式打开和关闭控制对象,探针类必须重写 getControlObjectName 方法,以便为所有控制对象解析显示名称。

拆分探针 (Split Probes)

拆分探针 (split probe) 的基类没有抽象方法,因为它可以仅用于调用树拆分 (call_tree_splitting),而无需添加探针视图 (probe view)。这种情况下,最小的探针定义如下:

public class FooSplitProbe extends SplitProbe {} 

拆分探针一个重要的配置项是是否可重入 (reentrant)。默认情况下,只会拆分顶层调用。如果你希望递归调用也被拆分,可以重写 isReentrant() 并返回 true。拆分探针还可以通过重写 isPayloads() 并返回 true,创建探针视图并将拆分字符串作为有效负载发布。

要执行拆分,需要成对调用 Split.enter()Split.exit()

public void splitMethod(String parameter) { 
    Split.enter(FooSplitProbe.class, parameter); 
    try { 
        performWork(parameter); 
    } finally { 
        Split.exit(); 
    } 
} 

与有效负载收集不同,拆分字符串必须和探针类一起传递给 Split.enter() 方法。同样,确保可靠调用 Split.exit() 很重要,因此应放在 try 的 finally 块中。Split 还提供了带 RunnableCallable 参数的 execute() 方法,可以通过单次调用完成拆分。

遥测 (Telemetries)

对于嵌入式探针来说,发布遥测 (telemetry) 特别方便,因为在同一个类路径 (classpath) 下,你可以直接访问应用中的所有静态方法。与注入式探针一样,在探针配置类中为静态 public 方法添加 @Telemetry 注解并返回数值即可。更多信息请参见 探针概念 (probe concepts) 章节。嵌入式和注入式探针 API 的 @Telemetry 注解是等价的,只是包名不同。

嵌入式和注入式探针 API 之间的另一个相似功能是可以通过 ThreadState 类修改线程状态。该类在两个 API 中都存在,只是包名不同。

部署 (Deployment)

在使用 JProfiler UI 进行分析 (profiling) 时,无需特殊步骤即可启用嵌入式探针 (embedded probes)。不过,只有在首次调用 PayloadSplit 时,探针才会被注册。此时,相关的探针视图 (probe view) 才会在 JProfiler 中创建。如果你希望探针视图从一开始就可见(如内置和注入式探针),可以调用

PayloadProbe.register(FooPayloadProbe.class); 

用于有效负载探针,

SplitProbe.register(FooSplitProbe.class); 

用于拆分探针。

你可能会考虑是否需要有条件地调用 PayloadSplit 的方法,比如通过命令行开关来最小化开销。但通常这不是必须的,因为这些方法体是空的。如果没有 attach profiling agent,除了构建有效负载字符串外不会有额外开销。考虑到探针事件 (probe event) 不应在微观层面频繁生成,它们的创建频率相对较低,因此构建有效负载字符串的开销可以忽略不计。

对于容器 (container) 场景,另一个关注点可能是不希望在类路径 (classpath) 上暴露外部依赖。你的容器用户也可能会使用嵌入式探针 API,从而导致冲突。在这种情况下,你可以将嵌入式探针 API 重定位 (shade) 到你自己的包中。JProfiler 仍然可以识别重定位后的包,并正确地对 API 类进行插桩。如果构建时重定位不可行,你也可以解压源码包,将类作为你项目的一部分。