登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java JFR 怎样记录自定义事件

来源:17golang原创

时间:2026-10-26 03:58:45 456浏览 收藏

如果一次订单处理偶尔变慢,普通日志通常只能告诉我们“结果失败了”或“耗时很长”,却不能把耗时、订单号和商品数量放进同一条 JVM 诊断记录。Java JFR 的自定义事件适合补上这层信息:继承 jdk.jfr.Event 定义事件和字段,再由 Recording 决定什么时候采集。

官方地址:https://docs.oracle.com/en/java/javase/25/docs/api/jdk.jfr/jdk/jfr/Event.html

这篇文章只解决一个边界:怎样定义并记录一个业务事件,不讨论把 JFR 当作日志系统,也不把示意图当成真实运行截图。

先把 Event、事件实例和 Recording 分开

可以把一次慢订单处理拆成三层。OrderLatency 是事件类型,描述有哪些字段;事件实例承载某一次订单处理的数据;Recording 决定这类事件是否进入当前采集范围并负责导出结果。三者混在一起时,最容易出现“代码调用了 commit,却在录制文件里找不到事件”的误判。

自定义事件应服务于一个诊断问题,例如“处理耗时是否与商品数量有关”。字段要围绕这个问题设计:订单标识用于关联业务,商品数量用于解释输入规模,持续时间用于判断慢点。不要把完整请求体、口令或大段异常文本默认塞进事件。

Java JFR 自定义 Event 类型、业务字段与 commit 记录入口的静态结构说明图
图1:自定义 Event 的静态结构说明图,查看事件类型、字段与记录入口的边界;不是运行截图。

继承 Event,并给字段补上可读元数据

下面的示例定义一个订单延迟事件。@Name 用于稳定的事件标识,@Label 和 @Description 让查看事件的人更容易理解字段。代码展示的是 API 用法,不在本机执行目录或业务任务。

import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;

@Name("com.example.OrderLatency")
@Label("订单处理延迟")
@Description("记录一次订单处理的输入规模与耗时")
final class OrderLatency extends Event {
    @Label("订单号")
    String orderId;

    @Label("商品数量")
    int itemCount;

    @Label("结果")
    String result;
}

事件字段应选择 JFR 支持的简单类型并保持体积可控。事件类是结构定义,不是每次请求都共享的全局状态;真正记录一次调用时再创建一个事件实例,给字段赋值后提交。

用 begin、end 和 commit 表达一次业务耗时

如果要记录持续时间,可以在业务操作前调用 begin(),结束后调用 end(),再调用 commit()。如果事件没有显式调用 end(),在提交时也会结束事件,但把边界写出来更容易让代码审查者看懂。

OrderLatency event = new OrderLatency();
event.orderId = orderId;
event.itemCount = itemCount;
event.begin();
try {
    // 这里代表实际业务调用,示例只说明事件生命周期。
    processOrder(orderId);
    event.result = "success";
} catch (RuntimeException ex) {
    // 只保留有限的结果标识,避免把敏感异常全文写进诊断事件。
    event.result = "failure";
    throw ex;
} finally {
    // 无论成功还是失败,都结束计时并提交这次事件。
    event.end();
    event.commit();
}

这里的 finally 是关键:成功、业务异常和提前退出都共享一个结束点。若事件只是表达瞬时事实,也可以不调用 begin/end,赋值后直接 commit()。

为什么提交前还要看 shouldCommit

事件对象很轻时,直接提交通常够用;如果字段需要格式化、序列化或计算昂贵摘要,可以先用 shouldCommit() 判断当前是否有录制会写入这类事件。它不是业务正确性的判断,也不代表事件一定已经写入文件,只是帮助调用方避免在没有采集需求时准备昂贵数据。

OrderLatency event = new OrderLatency();
event.orderId = orderId;

if (event.shouldCommit()) {
    // 只有可能被采集时才计算成本较高的诊断字段。
    event.itemCount = countItems(orderId);
    event.result = "success";
    event.commit();
}

这段写法的前提是业务代码不依赖事件字段去完成真正的业务逻辑。若 itemCount 同时是业务结果的一部分,就应先完成业务计算,再决定是否把它写进事件;不要因为没有 Recording 就跳过业务必需的数据。

Recording 决定采集边界,不是 Event 的替代品

要得到一个可分析的 JFR 文件,需要让 Recording 覆盖业务调用,再停止并导出。示例用 try-with-resources 管理 Recording,确保正常结束和异常路径都能关闭资源:

import java.nio.file.Path;
import jdk.jfr.Recording;

try (Recording recording = new Recording()) {
    // 开启当前录制范围;事件类型仍由自定义 Event 类定义。
    recording.start();

    OrderLatency event = new OrderLatency();
    event.orderId = "order-42";
    event.itemCount = 3;
    event.begin();
    try {
        processOrder(event.orderId);
        event.result = "success";
    } finally {
        // 让持续时间和提交动作处于同一业务边界。
        event.end();
        event.commit();
    }

    recording.stop();
    // dump 只导出本次 Recording 的数据,不等于线上页面验证。
    recording.dump(Path.of("order-latency.jfr"));
}
Java JFR Recording、enabled、shouldCommit、commit 与 JFR 文件的静态依赖结构说明图
图2:Recording 与事件提交边界说明图,查看采集控制和数据落盘的关系;不是运行结果截图。

如果 Recording 没有覆盖事件提交,或者该事件被设置为 disabled,文件中自然可能没有记录。事件没有出现时,应先检查采集边界、事件设置和提交点,不要先把问题归因于字段注解。

生产落地时保留四个约束

约束落地判断
事件粒度围绕一个诊断问题建模,避免每个普通日志都变成事件
字段成本限制字符串长度和计算成本,昂贵字段配合 shouldCommit
敏感信息不把口令、完整请求体和个人数据直接写入 JFR
资源边界Recording 用 try-with-resources,事件生命周期覆盖成功与异常路径

最后可以用一张清单排查:事件类是否继承 Event;字段是否回答诊断问题;持续时间是否在同一个 begin/end 边界内;提交前是否误把 shouldCommit 当成业务条件;Recording 是否覆盖了事件提交并正确停止。

常见问题

自定义 Event 一定要手动注册吗?

不一定。普通事件类可以按 JFR 的默认注册机制使用;只有需要控制注册时机或可见性时,才考虑 FlightRecorder.register 与 @Registered 这一组 API。

调用 commit 后为什么文件里没有事件?

先确认 Recording 正在运行并覆盖提交点,再检查事件是否被禁用以及事件名称是否与查看工具中的类型一致。commit() 表示提交请求,不等于任何时刻都必然产生可见记录。

总结一下:用 Event 描述一次诊断事实,用 begin/end 表示耗时边界,用 commit 写入,用 shouldCommit 控制昂贵准备,用 Recording 管理采集范围。这样自定义 JFR 事件才不会退化成另一种难以维护的字符串日志。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>