Foojay播客第103期:在构建时消除反射——对话Dmitry Moskalyuk与Shai Almog
运行时反射是 JVM 上那些不易察觉的隐性成本之一:它很灵活,无处不在,而且你很容易不再注意到它的存在,直到你开始追查 CPU 时间究竟花在了哪里。在本期节目中,来自生态系统两个截然不同角落的团队讲述了他们如何追查这个问题,并最终得出同一个答案:把这项工作提前到构建期。
Dmitry Moskalyuk 是 Foojay 的新作者。他的文章“JVM 上的无反射序列化:四种格式输入,Avro 输出”描述了一个模型层:它接收 JSON、XML、YAML 和 TOML,并将所有内容转换为 Avro;文章还讲述了在 Jackson 与 kotlinx.serialization 之间做选择的过程,而这一选择最终取决于反射成本究竟隐藏在哪里、又不在哪里。Shai Almog 自 2021 年起就在 Foojay 上撰写文章,他致力于 Codename One 的构建时代码生成流水线,目标是“无反射、无 Service Loader、无 Class.forName”。
我们比较了后端序列化层与 UI 框架的代码生成流水线,讨论了生成代码(Lombok、Manifold、jOOQ)是否是个好主意,审视了 Records 和 Project Leyden 带来了哪些改变,最后以最重要的问题收尾:在新项目中,你还应该动用反射吗?
YouTube
播客应用
你可以在以下平台收听并订阅 Foojay Podcast:
- Spotify
- Apple Podcasts
- 以及大多数其他平台……
嘉宾
- Dmitry Moskalyuk —— 高级后端工程师,在 JVM 上构建高负载分布式系统
- Shai Almog —— 作者、开发者关系与开源开发者,从事 Codename One 相关工作
链接
- JVM 上的无反射序列化:四种格式输入,Avro 输出(Dmitry 在 Foojay 上的文章)
- OpenAPI、ORM、SVG 与 Lottie(Shai 在 Foojay 上的文章)
- GitHub 上的 Jackson Blackbird
- Apache Avro
- Project Lombok
- Manifold
- jOOQ
- Project Leyden
内容
- 00:00 主题与嘉宾介绍
- 01:50 关于新的 Foojay,以及通过 GitHub pull request 投稿
- 03:17 什么是 Apache Avro
- 04:26 什么是 Codename One
- 06:31 Jackson 中的 Blackbird 是什么,以及它如何与 kotlinx 面向不同内容解析器的机制相比较
- 10:01 为什么 Codename One 聚焦于“无反射、无 Service Loader、无 Class.forName”
- 14:23 自动生成的代码不好吗(Lombok、Manifold、jOOQ)?
- 17:47 为系统带来改进的 Java 演进(Records、Project Leyden)
- 21:31 反射会随着 Java 版本变化吗?
- 24:29 构建期成本与运行时成本,Project Leyden
- 28:41 UI 与后端中反射的使用
- 32:16 将数据模型放在(Java)对象中的好处:单一事实来源、良好的调试体验
- 35:35 小项目需要担心序列化吗
- 37:38 序列化与安全
- 38:57 新项目中还应使用反射/序列化吗?
- 43:22 结语
文字记录
自动从音频生成,因此姓名和技术术语可能存在错误。建议更正。
[0:00] 本期播客涉及两个非常不同的 Java 项目。一个是后端数据层,另一个是跨平台 UI 框架。让我们看看它们究竟有什么共同点。
[0:10] 欢迎来到 Foojay 播客。这里有你需要的 OpenJDK 新闻。
[0:17] 欢迎回到 Foojay 播客。运行时反射一直是 JVM 上那些不易察觉的隐性成本之一。它很灵活。它无处不在。而且你很容易不再注意到它的存在,直到你开始追查 CPU 时间到底花在哪里。本期节目讲的是两个团队,他们在生态系统的完全不同的角落进行调查,却得出了同一个答案:把工作提前到构建期。本期有两位嘉宾,我邀请他们二人,是因为他们在 Foojay.io 上发表的文章。Dmitry Moskalyuk 是 Foojay 的新作者,最近发表了《JVM 上的无反射序列化:四种格式输入,Avro 输出》。在文章中,他介绍了一个模型层,它接收 JSON、XML、YAML 和 TOML,并将其全部转换为 Avro;文章还讲述了在 Jackson 与 KotlinX Serialization 之间做选择的过程,而这一选择最终取决于反射成本究竟隐藏在哪里、又不在哪里。Shai Almog 自 2021 年 9 月起一直在 Foojay 上发文。Shai,我回看了你的第一篇文章。现在文章数已经达到 140 篇。他致力于 Codename One 的构建时代码生成流水线,为 OpenAPI 客户端、ORM、JSON、XML 映射等生成带类型的 Java 代码。目标是:无反射、无 Service Loader、无 Class.forName。Dmitry、Shai,欢迎来到播客。Dmitry,特别欢迎你作为新的 Foojay 作者。我不知道这是否和我们离开 WordPress 后采用的新 Foojay 系统有关。你发现我们,是不是因为现在通过 GitHub pull request 投稿更容易了?
[2:07] 是的。嗨,感谢邀请我。很高兴来到这里。回答你的问题,我想我是在这些变化生效之前提交文章的。所以,是的,不过也许我现在会再多发表几篇文章。[笑声]
[2:26] 是的,这正是这个系统的期望——现在稍微容易一些了。Shai,你是常驻作者之一。你喜欢这种新方式吗?
[2:34] 我很喜欢。这是一个巨大的改进,我们也用这种方式改了自己的网站。其实回到反射这个问题,静态生成正是如此。这是一种更好的方法。我们在构建网站时就已经知道希望它长什么样,然后把运行时成本花在这上面,你懂的,所以这是同一类事情。
[3:03] 是的,这确实是个很好的类比。我们把 WordPress 在每次有人刷新页面时从数据库加载内容并渲染的工作,转移到了构建期。是的,这和文章有很好的相似之处。那么,我们进入文章吧。Dmitry,从你的文章开始。它讲的是 Avro。我之前不知道它。对我来说这是个新东西。你能解释一下这个 Avro 是什么吗?基本上,它就是一种让 Kafka 消化消息并压缩消息的格式。是的,所以这种格式围绕 schema,这样数据就可以被高效压缩。
[3:49] 嗯。它可以和 Protobuf 相比吗?那是我了解的东西。
[3:54] 是的。是的。它是在那之上的一层。
[3:58] 好。是的。你在文章里把它和 Jackson 做了比较?我们稍后会深入细节,但它和 JSON 以及 JSON 解析有很大区别吗?
[4:09] 我原以为会有区别,但结果并没有那么大,有时 Jackson 胜出,有时 Kotlin 胜出。
[4:19] 嗯。好。这取决于具体情况,这是这类问题中最重要、也最常得到的答案。Shai,我现在只选了一篇文章,讲的是 ORM、JSON 映射、SVG、Lottie 文件,因为它也触及了 Dmitry 提到的同一个话题。你能用两句话解释一下 Codename One 是什么吗?[哼声]
[4:47] 不能。[笑声]
[4:50] 好,给你三句。
[4:51] 它不断成长、不断扩展,我实在没法用两句话解释。抱歉。Codename One 本质上是一个全栈方案。它不再只是客户端了。我们现在也有后端实现。其理念本质上是实现 Java 一直承诺的“一次编写,到处运行”。这意味着我们比 Flutter 更具可移植性:我们覆盖每台设备,而且现在还覆盖后端;我们使用为移动设备构建的同一种 VM,因为服务器已经完全变了。
[5:29] 嗯。我们在 Foojay 上后来的一篇文章讲的是:用同样这套适用于 iOS、适用于 RAM 非常受限设备的技术,我们可以在内存占用、响应时间等方面提供与 Go 相当的性能,在启动时间和体积方面明显快于 GraalVM。
[6:00] 在编译方面也更快,并且可以生成一个完全自包含的小型可执行文件。要做到这一点,你需要移除反射,因为你需要能够摇树,找出应用中真正重要的部分。
[6:20] 好。现在回到文章。是的,关于 Codename One,Foojay 上有很多文章,人们真的可以花几个小时深入学习你写的一切。现在回到你的文章,Dmitry,它还提到把 Jackson 与 Blackbird 结合使用。这又是我从你那里学到的新东西。你能解释一下这个 Blackbird 在做什么吗?是的。基本上,Blackbird 是 Jackson 内部的一个机制,它会缓存反射 API,还会即时生成字节码来解析和存储数据。然后,如果我们进入文章中关于 Avro 的部分,你比较了什么?你发现反射存在什么问题?为什么在你所做的事情中它不是一个好主意?
[7:16] 我觉得显式总是优于隐式,对吧?
[7:21] 所以这是我在这个选择中的出发点。是的,我最初的想法是:无反射总是更好,如果它能工作,就会运行得更快;但事实并非完全如此。
[7:44] 嗯。嗯。问题是什么?因为你比较了所有不同格式,从 JSON、XML、YAML、TOML 一直到 Avro 格式。
[7:53] 是的。是的。所以基本想法是使用 KotlinX 的机制为所有输入格式生成解析器,简化工作,对吧?用 Jackson 做这件事看起来有点慢、有点迟钝,而 KotlinX 对团队来说又新又闪亮,所以我们决定试一试。
[8:27] 嗯。结果并不如你所料。
[8:31] 是的。没错。归根结底,KotlinX 缺少数据解析流程中的一个关键特性,我们不得不换回 Jackson。
[8:45] 嗯。
[8:46] 至少暂时如此。
[8:48] 嗯。你缺少的是什么特性?是关于保留未知字段,即不在 schema 中的字段。
[8:58] 好。所以你有 Jackson JSON 数据。它有一些未知字段,如果你把它导出回 JSON,你希望那些值再次出现在那里。
[9:07] 是的,我们想保留它们。这对我们试图实现的业务流程很重要。
[9:15] 你在 Jackson 里怎么解决?
[9:17] Jackson 对未知字段有特殊回调,你基本上可以把它们打包进某种类似 Map 的结构。
[9:25] 嗯。所以这个 Avro,我在开始录制前简单翻过文档。所以你必须预先定义结构,有点像 Protobuf 那样。
[9:39] 是的。完全像这样。
[9:41] 是的。是的。
[9:42] 是的。它会从这些定义生成 Java 代码吗?还是说它如何转换为 Java 代码?
[9:50] 我认为它会生成一些类来创建那些对象。它是某个库的一部分。
[10:01] 是的。说回你的工作,Shai,你的基线是无反射、无 Service Loader、无 Class.forName。为什么这是你正在做的事情?
[10:15] 一开始,我们需要构建一个高效的 Java 版本。当年在 Sun Microsystems,我们显然做过 J2ME,也做过 CDC、CLDC,那些版本不包含这些特性,而性能、体积、我们能进入的系统方面的差异非常明显。问题是,人们使用这些工具,而你会想:为什么我的生产服务器要重新发现我的编译器已经知道的东西?这是巨大的浪费。即使 JIT 能优化它,它也不能优化一切,而且它需要防止去优化,你仍然要为此付出代价。你要在启动时付出代价。你在所有方面都要付出代价。这个特性最初进入 Java 是为了 Java Beans 之类的东西,而一旦你有了某个特性,你就会使用它,就像老笑话说的:你的应用需要多少内存?你有多少内存?这几乎是同一回事。所以一旦我们有了反射,人们就会用它;一旦我们有了类加载器,人们就会把它当作穷人版隔离机制来用,而它并不适合做隔离。
[11:42] 但人们仍然使用它,仍然喜欢它。问题是,它被当作一种变通办法,用来处理那些运行多年、有时还会在线重新部署的服务器进程。现代服务器使用 Kubernetes、部署、Lambda 之类的东西。它们需要快速加载、快速下线,而且它们有各种外部防护机制,但现在我们无法真正利用这些机制,因为我们的服务器太浪费了。解决方案是在不使用这些东西的情况下构建,并构建更现代的东西,使其能够缩小到 Go 和类似工具的水平。这并不意味着这些工具不好。如果你有一个需要反射的系统——比如我也来自可观测性领域。在那里,没有反射、类加载这些东西,我们什么都构建不了。我们需要所有这些,这很好,如果你需要的话;但我遇到的大多数系统真的不需要。你有生产部署和预发布部署。仅仅为了切换这两个标志就使用反射,有点疯狂。
[13:01] 一旦我们以这些方式思考——我们在移动端不得不这样思考——它对后端也同样有意义。例如,我们的服务器是 GraalVM,它在某些方面是很棒的工具,但我会在 CI 上花 30 分钟,并为 CI 付费,主要因为 GraalVM 受制于 Java 对它的期望:要提供反射,要提供所有这些能力,而且它还因此生成一个非常大的二进制文件,因为 Spring 等下游的一切都期望如此。我们构建了一个服务器,基本上丢弃了所有这些,同时仍然提供类似 Spring 的接口。所以你仍然有依赖注入,仍然有类似 JPA 的 API。它不是 JPA,因为我们并不是真正的 Java。我们没有反射。
[14:00] 我们没有类加载。你用 Java 编写,但我们不能称它为 Java。不过它提供了你期望从 Go 这类语言获得的性能,同时仍然给你期望从 Java 获得的体验。这就是代价。所以反射并不是全部的代价,但它绝对是你最终要支付的账单的一部分。
[14:24] 嗯。你怎么看自动生成的代码?比如 Project Lombok,你可以通过一些注解,在项目构建时把代码注入系统。很多开发者不喜欢这种方法。
[14:44] 如今一切都是 AI 生成的,我们又绕回到这个问题。你怎么看这种生成代码,比如你在 Avro 或其他工具、Lombok 中显然可以拥有的那种?这仍然是一件坏事吗?
[15:00] 顺便提一下 Manifold,它比 Lombok 走得更远,也是一个相当惊人的项目。嗯,如果你用 JPA 而不用 Lombok,那有点痛苦,但我并不喜欢 Lombok。我想这是一种很常见的感受。不是因为它在某种程度上移除了封装等抽象,而是因为不用它 JPA 就很痛苦,而 Records 又不能作为实体,诸如此类。但是的,这是个难题,我的看法是,问题不在于 Lombok 这样的东西。Lombok 改变了 javac 的工作方式,这是一个有点问题的领域。我们做的主要是生成外部的东西,并在构建过程中把它们接进来。所以它主要是生成代码。[16:08] 在某些情况下,比如事务注解之类的场景,我们确实会为这些情况修改方法,因为 Spring API 期望如此,而且我们希望尽可能高性能。我们不想增加额外的层。我们会测量所有这些实际生成的汇编,以达到你期望的那种性能水平。我想我对 Lombok 也有类似的体验。我试过几次,但不喜欢。不过我有另一个代码生成库的体验,它做得很好,某种程度上替代了我们的 GPA 和 M。如果你听说过的话,是 Juke。
[16:57] Juke。是的。是的。是的。
[16:59] 是的。我记得那段经历很愉快,没有 Oil 的日子很特别。(哼声)
[17:09] 嗯。Juke 难道不是那种开源项目之一吗?它真正变成了产品,开发者可以靠自己所构建的东西生活。我觉得这是个很棒的工具。Simon Martinelli 也是它的忠实粉丝,也在 Foojay 上写关于它的文章。所以 Juke 是 Lombok,但更好。
[17:32] 嗯,Lombok 更多是关于通用代码编写,去除(清嗓子)
[17:40] 重复的代码行,而 Juke 严格来说是关于数据库数据
[17:45] 领域。是的。
[17:45] 是的。
[17:46] 是的。好的。你提到了记录(records)。那是 Java 演进之一,试图解决反复出现的问题,但不是所有问题,比如你提到的实体。启动时间方面,我们有一个即将到来的项目 Leyden,它会在启动方面带来相当大的改进。有一个想法,我不知道进展如何,就是将 JSON API 集成到 Java 中。Java 是否越来越擅长解决以前只能靠库解决的问题?
[18:24] 嗯,有了 Java 25,无论是启动时间还是其他方面都好了很多。我们做过基准测试,击败了 Java 8,这在性能方面已经需要相当的努力,但 Java 25 快得多。尤其是有了 AOT 改进,尽管我们现在的 AOT 并不是你提到的数据方面的完整故事,也不是真正的二进制文件,所以在启动方面、在所有方面、在大多数基准测试的总体开销方面,我们仍然用 AOT 击败 Java 25,显然不是所有情况,因为你无法击败 JIT。我认为 Java 的困难之一在于 Java 9,我认为我们在模块系统集成到 Java 的方式上走错了方向。
[19:26] 因为在那之前,我们构建的是单体 Java,这有它的优势,因为我们已经有一套隔离 Java 的机制,也就是包。这就是我们在 Code Name One 中尝试做的事情:我们尝试看待包——你使用这个东西,就只为那个东西付费。你不需要为其他任何东西付费。所以,我们不需要模块系统。我们不需要那些。但 Java 9 有点破坏了这一点。他们扔掉了 JAXB。他们扔掉了各种东西。现在他们又在某种程度上试图拉回一些东西,甚至对于字符串模板这样相对简单、早就该有的东西。你知道,格式化字符串是编程的 101,而我们仍然卡在 .format 那一套上。所以我认为那里存在一个文化问题,我理解。我喜欢 Java 的保守,但我不抱太大希望。
[20:34] 是的。Dmitry,你工作的公司用的是哪个 Java 版本?
[20:41] 21。是的,
[20:43] 21。好的。如果你看看 Java 的演进方式,为什么你还在用 21?为什么不用 25?21 对你正在做的事情来说足够好吗?
[20:56] 嗯,这是无尽积压的诅咒。你知道,(笑)总有更重要的事。是的。嗯,
[21:05] 如果他们用的是 21,那你就走在曲线前面了。所以,确实。这是个好答案。
[21:12] 是的,我正想说,你很幸运,没有待在一家还在用 Java 8、7 或 6 的公司,就像我最近在会议上和几个人聊过的那样(笑)。是的,你能享受到
[21:24] Java 5 兼容性的好处,你知道,所以这种情况还是有的。嗯。
[21:31] 整个反射的事情,它在不同 Java 版本中会改善吗?比如有了项目 Leyden,如果我们提前做测试运行,让应用启动更快,所有东西都已经被看到、加载和处理过了,这会有所不同吗?你知道这会如何演进吗?不,因为首先,它已经改善了。JIT 在优化掉一些开销方面做得非常出色。但有两个选择。要么你根本不需要反射,这意味着你只是把它当作一种抽象来用,而不是使用接口之类的。然后你就给 JIT 增加了无法收回的工作。你知道,JIT 需要做这些。每次它撤销一个优化——这比没做过汇编的人意识到的更频繁,因为很多优化会变坏。所以每次你都要为此付出代价,可能不是在你能感受到的即时 CPU 上,但它会被支付,因为 JIT 实际上非常擅长在多个核心之间拆分任务。所以你可能
[22:50] 在你运行的基准测试的底线中看不到,但它会以能源和其他成本的形式支付,因为它会从别处拿走资源。所以有代价,而且会一直存在。你可能不会像平常那样看到它,如今它真的非常高效,但高效是在运行一段时间后。初始启动方面,除了像你在 Leyden 中说的,如果他们缓存基准测试结果,我认为这种初始结果的方法有问题,因为你是在为基准测试优化,你得到的东西不错但不真实。例如,我们支持 DJ,这是做基准测试运行的一种语言方法,但它默认关闭,我们从不在基准测试中使用它,因为我觉得这有点像作弊,因为你运行的预热并不一定代表应用在实践中的表现。
[23:58] 所以,我们保留它是因为有些特殊情况可能确实会从中受益,但我认为它有点不确定,我不喜欢 Leyden 的那种做法。但这是个人意见。
[24:15] 换个说法,你认为使用反射是设计薄弱的标志,对吗?
[24:26] 所以回到架构师(笑)。如果我们看看你那边这个系统如何工作,Shai,你有一个 Maven 插件来处理代码,那你不是让构建时间更长了吗?还是说无论如何都是赢?
[24:46] 嗯,显然有构建时间成本。所以,把所有都加起来,它还是相对快的。例如,对比一下,我们在后端项目中做同样的事情,并在我们的 Foojay 文章中做了基准测试。我不确定文章是否已经发布,但我们还基准测试了构建时间,与 Spring 的 Java 版本比较,不是完整的后端版本。我们构建得比 Spring 快 30% 或 40%,你知道这不算多,因为 Spring 构建相当慢,但我们仍然——所以编译期间有成本,但我愿意支付编译期间的任何成本,不是任何,是大部分编译期间的成本,这样我的客户就不用付,我觉得这很公平。
[25:45] 我们是在 10 月 13 日录制这个。我看到你有一篇预定在 10 月 16 日发布的文章,标题是“一个更小更快的 Java 服务器,带有 Spring 风格的 API”。就是那篇。是的。所以到这个播客发布时,人们也可以在 Foojay 上读到它。Dmitry,关于构建应用所需的时间、运行时间、加载时间和预热时间,你使用文章中的工具后,是否看到了转变?
[26:14] 是的。嗯,我来自银行业,做高性能应用、可扩展架构,对我来说,构建时间不如性能和资源使用那么重要。所以(哼声)除非不可行,否则没关系。
[26:35] 你可以多花几分钟在构建和测试上。
[26:38] 是的,喝杯咖啡。(笑)
[26:40] 是的,喝杯咖啡。和同事聊聊天。但我认为项目 Leyden 将引入的是,你在 CI/CD 流程中有这个预热运行。是的,这又会稍微拉长构建流程,但你会赢。这是 Java 最终向云原生环境转变吗?这也是你试图用 Code Name One 解决的问题,例如
[27:11] 我不确定。我最喜欢 Leyden 的一点是小对象头,这在内存方面带来了巨大差异。我不认为从 8 到 4 的跳跃会有那么大区别,我们已经做过了,并没有真正显著改变。但跳到 8 字节对象头是巨大优势。问题是启动时间如此鲜明,即使与 GraalVM 相比也是如此,我认为后者始终会比 Leyden 更高效,因为它字面上就是原生代码。我认为 Leyden 在实际发布完整二进制编译时可能接近匹配,但它仍然是 JVM。而 JVM 在那个阶段不可能知道会发生什么。
[28:07] 它需要实际应用来运行。你无法真正欺骗这一点。而我们有点作弊了。这就是我们的解决方案。我们作弊的方式是说我们不是真正的 Java。不是那种方式。我们是 Code Name One。我们会给你相同的体验。你无法真正分辨差异。可口可乐还是百事可乐,你知道,盲测之类的。但是,我们会给你那个你正在付费的东西。我们会移除它,让你看看你能否分辨差异,以及那有多重要。
[28:40] 所以,在文章中,Dmitry,你做的是后端。在那篇文章中,Shai,你做的是 UI。这些系统之间仍然匹配吗?架构上我们需要现有代码、生成的代码,我们不介意,但不希望反射集成。这是你们俩考虑的差不多的事情吗?对我来说,更多是在使用某种神奇反射库与多花一点时间和精力构建足以满足我需求的解决方案之间的权衡。我认为历史上存在很大差异,确实最初的 JIT 有服务器模式和客户端模式,我们的看法是,客户端可以显示 GC 停顿,但它在解释器速度方面可以付出更多,因为只要性能比较平滑,你可能注意不到这些差异,而长时间运行的 JIT 在客户端中的价值要小得多。所以,你想要短期收益,不一定想要服务器中那种长期收益。问题是,过去 20 年所有这些都变了。服务器现在比以前更短暂。我们
[30:10] 不指望它们运行多年。我们有点在运行中更换它们。很多曾经适用于客户端的想法,比如小体积、更小的 RSS、GC 等等,都转变了,而且几乎普遍适用。所以服务器不是移动应用。仍然有根本差异:能源使用、内存限制等等。但当我们运行在容器中时,我可以在 200 到 300 兆字节的容器中运行服务器,现在这很重要,这不是我们历史上能做到的,但借助面向移动端的优化,甚至随着 ARM 的崛起——如果我们已经谈到这个——这很重要,我们看到那种意义上的整合越来越多。还有全栈。我认为这是一个巨大的卖点,而我错过了它。我怀念一路都有 Java,我希望我们能把它带回来。
[31:20] 是的,你可能知道我是 JavaFX 的忠实粉丝,但确实我缺少的是通往移动端和 Web 的全栈。是的,我发现 Vaadin 是 Web 部分的很好替代品,但然后我仍然缺少移动部分。顺便说一句,如果我们在谈论,我们的 JavaScript 前端是开源的,所以你也可以用它构建,并在 Web 上获得前端。还有即将推出的东西的剧透:我们在 Code Name One 中引入对 Swing 以及 JavaFX 一部分子集的兼容性。所以你可以实际拿现有的 JavaFX 应用,也许修改一些东西并运行它们。不过没有反射。
[32:07] Shai,你不仅是个好开发者和作家,还是你项目的好销售员。(笑)我们现在谈了很多关于运行时和 JIT 的内容。还有从开发者角度的观点。所以 Shai,你从 OpenAPI schema 文件转到 Java 对象。Dmitry,你对 Avro 文件做了同样的事。在开发时,将数据模型作为真正的 Java 对象、Java 代码,是一个很大的优势吗?
[32:41] 对我来说,更多是将事实来源放在一个地方。你知道,这让事情更容易理解得多。
[32:51] 那你的事实来源是什么?是 Java 到 Avro?还是 Avro 到 Java?嗯,基本上就是 schema,我们围绕 schema 生成代码,而 schema 是这一切的核心部分。
[33:07] 嗯。嗯。你呢,Shai?
[33:11] 你可能知道,我写过一本关于调试的书。所以对我来说,这是很大一部分,事实上,当我们生成某些东西时,我可以实际停下来,在调试器中单步执行,看到对象,它是真实对象,实际进入源代码,即使它是生成的,并理解那里发生了什么。这对我来说是个卖点,那种能力,而且我非常痴迷于可观测性等等。所以随着 AOT 编译失去这一点是有点痛苦的地方。所以我们通过代码生成在那里添加了某种无缝的 OTel 集成。所以你只需声明 OTel,就能得到它,你也可以调试它,这正是我在做的。你知道
[34:02] 调试,说得好。那和序列化有关吗?用不用它,它会让调试更容易。
[34:12] 我觉得关系不大,因为序列化、反序列化基本上可以很好地做单元测试。所以,
[34:22] 但我们有单元测试,然后还有生产环境中发生的事。(笑)
[34:25] 是的。所有性能问题、内存问题,我们在夜里遇到。是的。(喘气并哼声)
[34:33] Shai,你有一本关于这个的书,所以你在这里有明确观点。你知道,当单元测试失败时,你如何调试?你需要——以及你如何开发单元测试?你需要,比如,你设置一个断点,你看到“哦,我这里得到了这个”,然后你写代码来匹配你所拥有的,以防止潜在的回归或针对它进行测试。调试器往往是——你知道,人们把调试器看作只在有问题时使用的工具,而不是我们应该一直使用的工具。这是我的一个痛点:你知道,你应该在编写单元测试之前总是设置断点并验证断言。你应该熟悉代码的状态,因为你不一定期望某个变量有特定值,而当你查看它的实际状态时,它就有那个值。
[35:36] Dmitry,你说你在银行业工作,做非常重的应用。在什么情况下我应该担心是否使用序列化?这在小项目中是个重要话题吗?只在有大流量和处理大量数据的大项目中才重要吗?有阈值吗?为什么我应该考虑我的选择?
[36:03] 嗯,我认为这不太关乎性能,因为 Jackson 工作得非常好,尤其是那个 Blackbird 东西,它即时优化代码。我认为更多是关于你如何写代码,以及你的抽象有多干净,因为抽象越干净,调试和编写测试就越容易。
[36:32] 同样的问题问你。Code Name One 是用于大项目、小项目,还是所有项目?# Foojay 播客 #103:在构建时消灭反射——对话 Dmitry Moskalyuk 与 Shai Almog
[36:39] 嗯,这要看你怎么定义“一切”了。举例来说,我在企业环境和银行等机构里工作很多,很多银行用我们做客户端。我不确定我们在他们的后端是否也同样有意义,因为要与银行使用的一些遗留系统集成,未来几年我们可能还做不到,因为那是一个复杂的环境。但由于微服务等方式,即便在这些环境里也有空间。这也是我希望引入 OpenTelemetry 的原因之一:如今想进入任何企业环境,能有一条也把我们包含在内的调用链至关重要,而且它能与 Dynatrace、Datadog 以及你现有的任何工具集成。不过我想稍微谈谈序列化方面,以及这个意义上的安全性,因为如果你看过去几十年 Java 的几乎任何漏洞,都是从序列化、类加载器和反射开始的;它们可能不是漏洞的根源,但一定在利用链的某个环节上。
[38:04] 而这条利用链——当有人……这就是人们为这类我称之为“特性”、但其实是“能力”的东西所付出的巨大代价之一,而大多数人并不真正使用或需要它们,它们还带有这种隐藏的潜在缺陷。我记得 Log4j 出来时,我注意到 GraalVM 并不受影响,原因不是开发者有意为之,而是它不具备序列化外部对象并落入那个利用点的能力。它恰好没有这个能力。这一点非常重要。如果你在做反射,或者你团队里有人在用反射,那就是个问题。
[38:58] 所以,如果我现在启动一个全新的项目,用 Java 26、27,用最新最好的版本。我还应该用反射和序列化吗,还是尽量完全避免?嗯,我之前提到过,我们基本上不得不使用 Jackson,因为 Kotlin 当时没有处理未知字段的功能;但问题是,仓库里已经有一个完全针对这一点的开放合并请求。是的,我还会时不时去看看它有没有被合并。不过,从性能角度看,Jackson 工作得很好;如果你想成为新技术的采用者,那欢迎你。
[40:02] 好。Shai,你怎么看?我觉得你其实无法真正避开反射,就像你在用 Java 时无法避开呼吸一样,因为它就在那里,人们用过它,而且只要你使用任何下游依赖,就会引入反射,在大多数这类工具里这几乎不可避免。所以我认为 GraalVM 是一个很棒的 JVM,但拖累它的不是构建它的那些了不起的人的工作,而是整个 Java 生态的下游复杂性。比如你开始构建 Spring 这样的东西——再次说,Spring 是很棒的框架,我喜欢用它——但它带来的下游复杂性会把所有东西都拉进来,然后它就无法……我举个例子。我们的后端是用 Spring 和 GraalVM 构建的,我们需要那些依赖,然后就开始不断导入东西,最后我们的可执行文件有 250 MB,这对原生部署来说真的很糟糕。这不太好,而就目前而言,在普通 Java 里根本没有办法避免。如果你用 Codename One——显然这不是为了做过多的推销,虽然这是个重要项目。
[41:36] [笑声]
[41:38] 不过,人们可以在 Foojay 上读到所有相关内容。好,你还有什么想补充的吗?
[41:45] 有,有。我想补充一点:当我们处在快节奏、业务驱动的决策世界里时,Spring 开始变得更有意义,因为你可以更快地做原型。当然,它是有代价的,但如果你想快,它确实可以很快。嗯,确实如此。Shai。
[42:09] 哦,完全同意。这也是为什么我们在复制他们的 API。我百分之百喜欢 Spring,我认为 Spring 的很多能力设计得非常好、考虑得很周全。所以像 Spring Test 这些东西,我们尽量做到尽可能兼容。这样你就可以直接把测试拿过来用;当然,注解的包名之类的东西不同。但对开发者来说,它的接口非常棒。问题在于,你需要承担它的运行时开销。而且我认为像依赖注入这样的很多功能,如果你要做 Java EE 风格的部署——在运行时替换各种东西——那你就需要运行时开销。但在大多数真实的生产环境中,你真的需要这些吗?如果你在构建银行应用,也许吧;如果你要进入历史上 Java EE、J2EE 那一套,也许吧。
[43:10] 但如今我们大多数用 Java 构建应用的人,可能并不真正需要所有这些,也根本不用这些功能,却要为它们付出代价。所以,研究一下你有哪些可能性,然后选对的那个。这完全取决于你实际需要什么、应该用什么。然后,是的,我们还有你写的那些好文章,帮助人们决定该用什么、该怎么构建。好,非常感谢你抽出时间。也非常感谢你收听这期播客。本期节目就到这里。Dmitry 和 Shai,谢谢你们二位,展示了来自 GraalVM 生态系统非常不同角落的两个人,如何得出了关于反射的相同结论,也谢谢你们坦诚地说明编译时代码生成仍然可能让你付出什么代价。感谢听众们收听。如果你喜欢这次对话,请在 YouTube 或你常用的播客应用中订阅 Foojay 播客,这样就不会错过下一期。欢迎到 Foojay.io 查看更多播客,以及 Dmitry、Shai 和其他 Foojay 优秀作者的文章。谢谢。
[44:24] 给我一个 J。给我 OpenJDK。
撰文作者
Dmitry Moskalyuk
资深后端工程师,拥有 10 年以上 JVM 高负载分布式系统设计与扩展经验。我参与过从零构建的平台,也做过金融科技、网络安全和大规模媒体基础设施领域的复杂遗留系统迁移。
Frank Delporte
Frank Delporte 是 Java Champion、Java 开发者、Azul 高级技术文档工程师、博主,著有《Java Programming for Raspberry Pi - A Hands-On Guide to Electronics and IoT Projects》,也是 Pi4J、Lottie4J、Sheetmusic4J 等项目的开源贡献者……
Shai Almog
作者、开发者关系从业者、博主、开源黑客、Java 摇滚明星、会议演讲者、讲师和创业者。
相关文章
- JVM 上的无反射序列化:四种格式输入,Avro 输出 —— Dmitry Moskalyuk,2026 年 9 月 10 日,356 次浏览(数据工程)
- OpenAPI、ORM、SVG 与 Lottie —— Shai Almog,2026 年 6 月 18 日,590 次浏览(Java)
- 为什么序列化 Java 字段的最快方式是完全不序列化它 —— Per Minborg,2022 年 10 月 25 日,10,191 次浏览(DevOps)
- Foojay 播客 #102:为什么 Foojay 从 WordPress 转向静态网站 —— 3 位作者,2026 年 10 月 5 日,173 次浏览(播客)## 参与讨论
数据工程 [LOADING...]
[LOADING...]Dmitry Moskalyuk2026年9月10日 356 次浏览
JVM 上的无反射序列化:四种格式输入,Avro 输出
数据工程
Java [LOADING...]
[LOADING...]Shai Almog2026年6月18日 590 次浏览
OpenAPI、ORM、SVG 与 Lottie
Java
DevOps [LOADING...]
[LOADING...]Per Minborg2022年10月25日 10,191 次浏览
为什么序列化 Java 字段的最快方式是根本不序列化它
DevOps
播客 [LOADING...]
[LOADING...][LOADING...][LOADING...]3 位作者
2026年10月5日 173 次浏览