如果您可以控制作为探针 (probe) 目标的软件组件的源代码,则应编写嵌入式探针,而不是注入式探针。
编写注入式探针时,大部分前期工作都花费在指定被拦截的方法,以及选择被注入的对象作为处理程序方法(handler method)的方法参数上。对于嵌入式探针,这没有必要,因为您可以直接从被 monitor 的方法中调用嵌入式探针 API。嵌入式探针的另一个优势在于部署是自动的。探针会随您的软件一起分发,并在应用程序被分析 (profiled) 时显示在 JProfiler UI 中。您唯一需要随附分发的依赖项是一个遵循 Apache 2.0 许可证的小型 JAR 文件,它主要由作为 profiling agent 钩子的空方法体组成。
开发环境
开发环境与注入式探针相同,区别在于构件名称是 jprofiler-probe-embedded 而不是
jprofiler-probe-injected,并且您需要将该 JAR 文件随应用程序一起分发,而不是在单独的项目中开发探针。向软件组件添加嵌入式探针所需的探针 API 包含在单个 JAR
构件中。在探索 API 时,您可以从 Javadoc 中 com.jprofiler.api.probe.embedded 的包概述开始。
与注入式探针类似,嵌入式探针也有两个示例。在 api/samples/simple-embedded-probe 中,有一个帮助您开始编写嵌入式探针的示例。在该目录中执行
../gradlew 即可对其进行编译和运行,您可以研究 Gradle 构建文件 build.gradle 以了解执行环境。有关更多功能(包括控制对象
(control object)),请参见 api/samples/advanced-embedded-probe 中的示例。
有效负载 (payload) 探针
与注入式探针类似,您仍然需要一个探针类用于配置。探针类必须继承 com.jprofiler.api.probe.embedded.PayloadProbe 或
com.jprofiler.api.probe.embedded.SplitProbe,具体取决于您的探针是收集有效负载 (payload) 还是拆分调用树 (call tree)。在注入式探针
API 中,您可以在处理程序方法上使用不同的注解来进行有效负载收集和拆分。另一方面,嵌入式探针 API 没有处理程序方法,需要将这些配置转移到探针类本身。
public class FooPayloadProbe extends PayloadProbe {
@Override
public String getName() {
return "Foo queries";
}
@Override
public String getDescription() {
return "Records foo queries";
}
}
注入式探针使用注解进行配置,而嵌入式探针则是通过重写探针基类中的方法来进行配置。对于有效负载探针,唯一的抽象方法是
getName(),所有其他方法都有默认实现,您可以根据需要进行重写。例如,如果您希望禁用事件视图 (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 调用,调用树将被破坏,因此这应该始终在 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(FooPayloadProbe.class, query, () -> query.execute());
}
接收 Callable 的签名的问题在于 Callable.call() 会抛出已检查的异常 (exception),因此您必须捕获它或者在所在方法上声明抛出它。
控制对象 (control object)
有效负载探针可以通过调用 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);
}
}
必须在探针配置中显式启用控制对象视图 (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 MyEventTypes.class;
}
}
如果您没有显式打开和关闭控制对象,探针类必须重写 getControlObjectName,以便解析所有控制对象的显示名称。
拆分探针
拆分探针基类没有抽象方法,因为它可以仅用于拆分调用树 (call tree) 而不添加探针视图 (view)。在这种情况下,最小的探针定义仅为
public class FooSplitProbe extends SplitProbe {}
拆分探针的一个重要配置是它们是否应该可重入。默认情况下,只有顶层调用会被拆分。如果您希望也拆分递归调用,请重写 isReentrant() 并返回
true。如果您在探针类中重写 isPayloads() 并返回 true,拆分探针还可以创建探针视图并将拆分字符串作为有效负载 (payload)
发布。
要执行拆分,请成对调用 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 还提供了带有 Runnable 和 Callable 参数的
execute() 方法,可以通过单次调用来执行拆分。
遥测 (telemetry)
为嵌入式探针发布遥测 (telemetry) 特别方便,因为处于同一个类路径 (classpath) 中,您可以直接访问应用程序中的所有静态方法。就像注入式探针一样,在探针配置类中使用
@Telemetry 注解静态公共方法并返回数值。有关更多信息,请参见关于探针概念的章节。嵌入式和注入式探针
API 的 @Telemetry 注解是等效的,它们只是位于不同的包中。
嵌入式和注入式探针 API 之间的另一个对应功能是使用 ThreadState 类修改线程状态的能力。同样,该类存在于两个 API 中,只是包名不同。
部署
使用 JProfiler UI 进行分析 (profiled) 时,无需执行任何特殊步骤即可启用嵌入式探针。但是,只有在首次调用 Payload 或 Split
时,探针才会被注册。只有到那时,JProfiler 中才会创建关联的探针视图 (view)。如果您希望探针视图从一开始就可见(就像内置探针和注入式探针那样),您可以对有效负载探针调用
Payload.register(FooPayloadProbe.class);
以及对拆分探针调用
Split.register(FooSplitProbe.class);
。
您可能正在考虑是否按条件调用 Payload 和 Split 的方法,例如通过命令行开关进行控制以尽量减少开销。然而,这通常是没有必要的,因为方法体是空的。在没有
attach profiling agent 的情况下,除了构建有效负载字符串之外,不会产生任何开销。考虑到探针事件不应该在微观尺度上生成,它们被创建的频率相对较低,因此构建有效负载字符串的开销应该微不足道。
对于容器 (container) 而言,另一个顾虑可能是您不希望在类路径 (classpath) 上暴露外部依赖项。容器的用户也可能会使用嵌入式探针 API,这会导致冲突。在这种情况下,您可以将嵌入式探针 API shade 到您自己的包中。JProfiler 仍会识别被 shade 的包并正确对 API 类进行插桩。如果构建时 shading 不可行,您可以解压源码归档并将这些类作为项目的一部分。