将 Joda-Time 迁移至 java.time
迁移代码(即遗留代码)绝非易事。要完成这项工作,需要投入大量的规划和精力。虽然这对开发人员来说并非最令人兴奋或最具动力的工作,但将遗留代码迁移到新库版本确实需要决心和丰富的经验。Joda-Time 到 java.time 的迁移就是这样一项需要缜密规划和执行的任务。
如果您的 Java 项目在 Java SE 8 之前启动,并且涉及日期/时间处理,那么它很可能使用了 Joda-Time——这是在 SE 8 之前处理日期和时间函数的优秀库,也是事实上的标准。如果您的项目仍然在使用 Joda-Time 并希望迁移到 java.time,请继续阅读。
Java SE 8 的发布引入了一个改进后的标准日期和时间 API,通常称为 java.time (JSR-310)。目前,Joda-Time 项目官方建议迁移至 java.time (JSR-310)。
虽然 java.time (JSR-310) 在很大程度上借鉴了 Joda-Time,但它并不向后兼容,且概念和术语也发生了变化。因此,从 Joda-Time 迁移到 java.time 需要仔细检查每一行代码。这个过程非常耗时,甚至会让人希望有一种更简单、自动化的迁移方式。
现在有了更好的迁移方法,我们利用 Sensei 创建了解决方案——这是一个 IntelliJ 插件,可以根据您定义的配方(规则)自动执行代码转换。将时间花在定义可复用的配方上,而不是重复执行迁移任务。这种自动化不仅能转换您的遗留 Joda-Time 代码,还能帮助团队在编写新代码时直接在 IDE 中遵循规范。
为了帮助您快速上手,我们创建了一个公开的 Sensei 配方集Standardization on java.time (JSR-310),其中包含了以更轻松的方式从 Joda-Time 迁移到 java.time 的配方。这是一个不断增长的配方库,我们将持续扩展以涵盖更多场景。
以下是一个迁移示例,可以帮助您了解 Sensei 如何让遗留代码迁移变得轻松无痛。

从重复的手动迁移到自动化代码转换
让我们看一个创建新 DateTime 的示例,它展示了在将单行代码从 Joda-Time 迁移到 java.time 时会遇到的一些隐蔽陷阱。随后,我们将查看来自“Standardization on java.time (JSR-310)”配方集中的一个 Sensei 配方,展示它如何捕获所有这些信息,以便任何开发人员都能反复使用相同的迁移逻辑。
在此示例中,我们使用 7 个代表 DateTime 字段值的整数参数来构建一个 Joda-Time DateTime 对象。
我们该如何将其迁移到 java.time 的等效实现呢?
Joda-Time 中该构造函数的 javadoc 说明如下:
使用默认时区中的 ISOChronology,根据日期时间字段值构造一个实例。
起初,我们可能会假设 java.time 中也有一个DateTime 类,但实际上并没有。如果您在谷歌搜索“migrate from joda time to java time”,很可能会找到 Stephen Colebourne 的博文Converting from Joda-Time to java.time。
这为您提供了一个良好的开端,并指引我们使用 java.time.ZonedDateTime 或 java.time.OffsetDateTime。现在出现了第一个问题:我该用哪一个?根据 Stephen 的建议,很可能是 ZonedDateTime。
查看ZonedDateTime 的 javadoc,我们发现根本没有任何构造函数。回到 Stephen 的博文,我们继续往下读:
构造。Joda-Time 提供了一个接受 Object 并执行类型转换的构造函数。而 java.time 仅提供工厂方法,因此转换工作需要用户自行处理,不过它为字符串提供了 parse() 方法。
因此必须存在一个静态工厂方法。在搜索静态方法时,我们找到了一个看起来很接近的,但并不完全相同。
它和我们最初的 Joda-Time DateTime 构造函数一样有 7 个 int 参数,但如果你不仔细看,就会错过一个重要细节。第 7 个参数不再代表毫秒,而是纳秒,这是因为 java.time 比 Joda-Time 精度更高,以纳秒为单位测量时间。这是一个很容易被忽略的重要细节。此外,该方法需要一个 ZoneId,这不禁让人疑惑:为什么之前不需要,现在却需要了?
回想一下我们最初那个构造函数的 Javadoc,它提到会使用默认时区,也许有办法获取默认的 ZoneId?
ZoneId 的 javadoc 没有列出任何构造函数,但查看静态方法,我们发现可以使用 systemDefault()
现在我们已经搞定了 ZoneId,那么毫秒到纳秒的转换该怎么办呢?也许我们可以使用 java.util.concurrent.TimeUnit 来执行转换。
该方法返回一个 long 类型,而我们的方法需要 int 类型,所以现在我们又面临一个转换问题。也许我们可以尝试简单一点的方法。比如乘法?
这样做虽然可行,但看起来有点不协调。如果你还没注意到,我们已经花费了大量的时间和精力来迁移这一行代码。可以想象,我们还有许多这样的编辑工作需要手动完成,而且情况并不会好转。
然而,如果我们深入研究 java.time API,就能发现一个看起来更流畅的解决方案。
虽然 ZonedDateTime 没有直接设置毫秒的方法,但可以使用 with(TemporalField field, long newValue) 方法,并将 ChronoField.MILLI_OF_SECOND 作为 TemporalField 参数。
Java 文档中提到,它会为我们自动执行到纳秒的转换:
当该字段用于设置值时,其行为应与设置 NANO_OF_SECOND 并将值乘以 1,000,000 的效果相同。
因此,我们只需在工厂方法中将纳秒指定为 0,然后使用 with 方法创建一个包含所有原始值以及毫秒的 ZonedDateTime 对象即可。
看看最终结果,虽然看起来只改动了一行代码,但这并不能体现我们在研究这次迁移时所付出的努力!
创建配方以实现更快速、更轻松的迁移
Sensei 提供了一种与其它开发者分享这些宝贵经验的方法。通过创建一个涵盖所有这些要求的配方,Sensei 用户只需点击鼠标即可完成此项迁移。
Sensei 配方由 3 个主要部分组成:
- 元数据
- 搜索
- 可用修复
让我们来看一个 Sensei 配方(也可以视为 YAML 配方),它将帮助我们将此调用迁移到其 java.time 等效项。
DateTime foo = new DateTime(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
元数据部分
元数据部分包含有关配方及其使用方法的信息。
搜索部分
Sensei 配方的搜索部分指定了该配方应应用于哪些代码元素。
search:
instanceCreation:
args:
1:
type: int
2:
type: int
3:
type: int
4:
type: int
5:
type: int
6:
type: int
7:
type: int
argCount: 7
type: org.joda.time.DateTime
在此搜索部分中,我们可以看到:
- 正在搜索 instanceCreation, 即构造函数的使用。注意:还有许多其他可用的搜索目标
- 构造函数应具有 7 个参数,这由 argCount 属性指定,
- args 1-7 应为 int 类型
- 我们正在搜索 type 为 org.joda.time.DateTime 的构造函数
可用修复部分
availableFixes 部分可以指定一个或多个可应用于匹配代码元素的修复程序。每个修复程序可以包含多个操作,在我们的示例中,我们有一个执行 2 个操作的单一修复程序。
- The name of the fix is shown to the user in the ‘Quickfixes’ menu, and describes what will happen if the user applies this quickfix
- The actions list shows what actions will be performed by this quickfix
- The rewrite action will rewrite the code element using a mustache template. It can make use of variables and string replacement functions.
- The modifyAssignedVariable action will check to see if this constructor is being used to assign the value to a variable. If so, this action will modify the variable to be declared as the type specified by type
使用配方进行代码转换
编写并启用配方后,它会扫描我们的代码并高亮显示可以应用该配方的代码段。
在下方的截图中,我们可以看到目标构造函数已被 Sensei 标记。将鼠标悬停在标记的构造函数上,会显示配方的 shortDescription 和快速修复选项 迁移至 java.time.ZonedDateTime

选择 迁移至 java.time.ZonedDateTime 快速修复后,代码将根据我们在配方中指定的操作进行转换。

一次性迁移与团队统一编码实践——尽在 Sensei
从上面的示例可以看出,单行代码的迁移可能涉及来之不易的专业知识。Sensei 可以将这些知识转化为可在团队内共享的可执行配方或手册。您可以规划一次性迁移冲刺,或者在遇到 Joda-Time 代码时采取增量式即时转换到 java.time 的方法。您可以启用/禁用配方,以逻辑阶段或步骤进行迁移,甚至可以扩展或缩小 Sensei 扫描文件的范围——这种灵活性让代码迁移不再痛苦。
库迁移只是 Sensei 用于标准化项目众多方式中的一个例子。您可以随时留意在代码审查或自行编码时经常遇到的反模式或特定的手动代码转换。如果您有一套开发人员经常忽略的 编码指南,那么您可以将这些指南转换为配方,使开发人员能够自信地应用经批准的代码转换。
如果您有任何疑问,欢迎随时联系我们!请加入我们的 Slack:sensei-scw.slack.com
在 AI 驱动的开发成果发布前实现有效治理
评估 AI 辅助风险,在提交代码时强制执行安全编码策略,并加速整个软件开发生命周期的安全交付。
探索更多博客
获取有关安全编码、人工智能治理和软件风险管理的专家内容。
%252520%252520(3).avif)
Supercharged Security Awareness: How Tournaments are Inspiring Developers at Erste Group
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

Security as culture: How Blue Prism cultivates world-class secure developers
Learn how Blue Prism, the global leader in intelligent automation for the enterprise, used Secure Code Warrior's agile learning platform to create a security-first culture with their developers, achieve their business goals, and ship secure code at speed
在产品发布前确保 AI 驱动的开发安全
洞察开发风险,执行安全策略,并在整个软件开发生命周期中预防漏洞。



.avif)