如果你能够控制目标软件组件的源代码,建议编写嵌入式探针 (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.PayloadProbe 或
com.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 还提供了带 Runnable 和 Callable 参数的
execute() 方法,可以通过单次调用完成拆分。
遥测 (Telemetries)
对于嵌入式探针来说,发布遥测 (telemetry) 特别方便,因为在同一个类路径 (classpath) 下,你可以直接访问应用中的所有静态方法。与注入式探针一样,在探针配置类中为静态 public 方法添加
@Telemetry 注解并返回数值即可。更多信息请参见 探针概念 (probe concepts)
章节。嵌入式和注入式探针 API 的 @Telemetry 注解是等价的,只是包名不同。
嵌入式和注入式探针 API 之间的另一个相似功能是可以通过 ThreadState 类修改线程状态。该类在两个 API 中都存在,只是包名不同。
部署 (Deployment)
在使用 JProfiler UI 进行分析 (profiling) 时,无需特殊步骤即可启用嵌入式探针 (embedded probes)。不过,只有在首次调用 Payload 或
Split 时,探针才会被注册。此时,相关的探针视图 (probe view) 才会在 JProfiler 中创建。如果你希望探针视图从一开始就可见(如内置和注入式探针),可以调用
PayloadProbe.register(FooPayloadProbe.class);
用于有效负载探针,
SplitProbe.register(FooSplitProbe.class);
用于拆分探针。
你可能会考虑是否需要有条件地调用 Payload 和 Split 的方法,比如通过命令行开关来最小化开销。但通常这不是必须的,因为这些方法体是空的。如果没有
attach profiling agent,除了构建有效负载字符串外不会有额外开销。考虑到探针事件 (probe event) 不应在微观层面频繁生成,它们的创建频率相对较低,因此构建有效负载字符串的开销可以忽略不计。
对于容器 (container) 场景,另一个关注点可能是不希望在类路径 (classpath) 上暴露外部依赖。你的容器用户也可能会使用嵌入式探针 API,从而导致冲突。在这种情况下,你可以将嵌入式探针 API 重定位 (shade) 到你自己的包中。JProfiler 仍然可以识别重定位后的包,并正确地对 API 类进行插桩。如果构建时重定位不可行,你也可以解压源码包,将类作为你项目的一部分。