1

我在 Linux 上运行的嵌入式 Java 应用程序应该允许用户通过 GUI 更改时区。我的应用程序在 OSGI 容器中运行(请参阅下面为什么我认为这是相关的)并且在使用新时区之前不需要重新启动。

从我的 Java/OSGi 应用程序中持久设置时区的推荐方法是什么?

我可以想到以下方法,为此我列出了一些优点和缺点。我错过了什么吗?有什么推荐?:

  1. 从应用程序中,更改底层操作系统时区,另外TimeZone.setDefault(...)用于当前运行的 JVM 并更新所有Clock持有旧 TZ 的实例(因此需要某种事件)。缺点:这种方法依赖于操作系统并且级别很低,我也想保持操作系统时钟 UTC。优点:操作系统负责存储 TZ,下次启动应用程序时 TZ 会立即正确。
  2. 从应用程序中,更改-Duser.timezone=...用于启动的参数。缺点:非常丑陋,甚至更低级别,但允许将操作系统时钟保留为 UTC,同时让应用程序以正确的 TZ 启动。还需要Clock在更改时更新实例。
  3. 不要触摸操作系统,只在启动的早期TimeZone.setDefault(...)使用和调用它。这将需要一个单独的持久性(首选项)来保存它。同样,在当前运行的 JVM 中,所有引用旧 TZ 的实例都需要在更改时更新(需要事件)。在 OSGi 容器中运行时,无法保证包的启动顺序,因此我无法确定默认 TZ 在使用之前是否已设置。我如何保证这一点?此外,JSR310 明确建议不要在 Clock 中使用“默认 TZ”。Clock
  4. 根本不使用“默认”时区,使用单独的全局变量,并在每次InstantLocalXXX值之间转换时,显式传递时区。这摆脱了需要一个事件来更新Clock实例。但是我们需要注意不要使用LocalDate.now(clock),因为这使用了时钟的 TZ (不再正确)。如何在 OSGi 中有这个全局变量?使用ConfigAdmin?如何使我无法控制的代码行为正确(例如记录时间戳)?

编辑:为了摆脱更新的需要Clock,我可以使用一个始终检查默认时区的时钟,但这从性能 POV 来看似乎不是最理想的:

public class DefaultZoneClock extends Clock {
  private final Clock ref = Clock.systemUTC();

  @Override
  public ZoneId getZone() {
    return ZoneId.systemDefault(); // probed on each request
  }

  @Override
  public Clock withZone(ZoneId zone) {
    return ref.withZone(zone);
  }

  @Override
  public Instant instant() {
    return ref.instant();
  }
}

这是一个好主意吗?

编辑 2

关于我上面的性能问题:它们显然是不合理的。当你调用时LocalDate.now(),内部SytemClock会构建一个新的,ZoneID它通过在 Map 中搜索来设置当前 - 这与使用我上面的代码完全相同DefaultZoneClock,不同之处在于使用我的代码我可以注入任何其他代码Clock进行测试。(所有客户端代码都将使用LocalDate.now(clock)

下面的答案建议不要更改 JVM TimeZone,而是在必要时根据用户定义的 TimeZone 进行转换,这意味着我必须注意不要使用调用 TimeZone 的 java.time 方法Clock,例如。如果我需要LocaTime使用

// OK, TimeZone is set explicitely from user data
LocalTime t = clock.instant().atZone(myUserZoneID).toLocalTime();

// Not OK, uses the Clock's internal TimeZone which may not have been set or updated
LocalTime t2 = LocalTime.now(clock);
4

2 回答 2

3

根据我的经验,日期/时间应始终在内部使用 UTC 表示。仅在向用户显示时转换为本地时间。此时,您还需要根据用户的语言环境对其进行格式化。

所以问题就变成了,你怎么知道用户的时区和语言环境?这取决于应用程序的性质。如果它是单用户桌面应用程序,那么您应该在操作系统中查找这些或使用存储在 Config Admin 服务中的配置。如果它是一个多用户 Web 应用程序,那么您可能会在 Web 会话中包含一些与用户相关的信息;或使用 Accept-Language 标头甚至地理位置。

更新

海报澄清说,该应用程序是嵌入式设备上的单用户。在这种情况下,每次需要显示时间时查询当前系统时区不是更容易吗?这避免了令人讨厌的全局变量,并且还允许您的应用程序动态响应时区的变化,例如,如果用户将设备从一个地方带到另一个地方。

我不认为您应该TimeZone.setDefault从您的 Java 应用程序中调用,因为您将从哪里获得这些信息?也许直接用户输入,但用户不喜欢在操作系统中设置它,以便所有应用程序都可以获取它吗?

于 2015-01-12T14:38:28.243 回答
0

你似乎是在鼹鼠丘上造山,努力把一个简单的问题变成一个复杂的问题。

将日期时间值视为本地化文本

本地化日期时间是一个本地化问题。以类似于某些用户会说法语、一些意大利语和一些德语的方式来处理它。

使用一种语言(例如英语)完成所有内部工作,例如查找和键值,即使您的用户都不会说英语。

对于日期时间,相当于将内部值保留在 UTC 时区。您的业​​务逻辑、文件存储和数据库记录都应该使用 UTC。如果知道原始时区和/或原始输入很重要,请将其存储为除 UTC 调整值之外的附加信息。

当需要向用户显示或为用户导出数据时,您将使用这些英文字符串和键来查找法语、意大利语或德语翻译。哪种语言?三种可能:

  • 用户计算环境的默认语言(JVM 默认值、http 标头、JavaScript 查询等)
  • 用户明确选择的语言(我们的应用询问用户)
  • 适合数据上下文的语言。

对于前两个,您以某种方式为每个用户保留了个人资料。在 Web 应用程序中,我们使用会话对象。在单用户“桌面”应用程序中,我们使用 Singleton。对于用户选择的语言,持久保存到数据库或文件存储中,以便记住该用户未来的工作会话。

日期时间也是如此。在呈现或导出数据时,如果上下文未指定时区,则从其计算环境中检测其当前时区。如果时区很重要,或者用户移动性很强并且跨越时区,那么请询问用户他们选择的时区。提供正确的时区名称列表。请记住此用户在未来工作会话中的选择。

在日期时间对象上指定时区

Java 8 中的 Joda-Time 库和 java.time 包都可以轻松地将日期时间对象的指定时区与 UTC 进行调整。您应该在此处更改数据值对象的时区,而不是更改 JVM 的默认值或操作系统区域设置。

登录 UTC

至于日志记录和其他系统问题,应该在 UTC 中完成。您可以稍后在调查问题时将该 UTC 值转换为本地时区。或者在相关的日志事件消息中包含用户的本地化日期时间。

不要乱来Clock

在生产中部署时,我不会弄乱 java.time Clock。这门课的主要目的是测试。您可以插入一个虚假的时钟来谎报当前日期时间以创建测试场景。您可以在生产中插入时钟,但对我来说,在日期时间对象上指定所需的时区更有意义。

例子

例如,假设用户希望在上午 7:30 发出警报。您可以询问用户或以其他方式确定他们想要的时区在蒙特利尔。这是 Joda-Time 中不切实际的代码(java.time 类似)。

DateTimeZone zone = DateTimeZone.forID( "America/Montreal" );
DateTime alarmMontréal = DateTime.now( zone ).plusDays( 1 ).withTime( 7 , 30 , 0 , 0 );
DateTime alarmUtc = alarmMontréal.withZone( DateTimeZone.UTC );

我们alarmMontréal向用户显示,但存储alarmUtc在数据库中。两者都代表了宇宙历史时间轴上的同一时刻。

假设用户同时飞往美国俄勒冈州波特兰市。如果我们希望警报在同一时刻触发不变,则无需进行任何更改。为了显示该警报在波特兰时间何时触发,我们创建了一个新的不可变对象以适应另一个时区。

DateTime alarmPortland = alarmUtc.withZone( DateTimeZone.forID( "America/Los_Angeles" ) );  // 3 hours behind Montréal, 04:30:00.000.

如果我们当时要打电话给我们在印度的叔叔,我们会使用此代码的结果向他发送一封预约确认电子邮件:

DateTime alarmKolkata = alarmUtc.withZone( DateTimeZone.forID(  "Asia/Kolkata" ) );

我们有四个 DateTime 对象,它们都代表宇宙时间轴中的时刻:

zone            America/Montreal
alarmMontréal   2015-01-14T07:30:00.000-05:00
alarmUtc        2015-01-14T12:30:00.000Z
alarmPortland   2015-01-14T04:30:00.000-08:00
alarmKolkata    2015-01-14T18:00:00.000+05:30

当地时间

如果在不同的场景中,您希望用户碰巧所在的本地时区为 7:30,那么您将使用 a LocalTime,这只是任何时区早上 7 点 30 分的想法。ALocalTime与宇宙历史的时间线无关。

Joda-Time 和 java.time 都提供了 LocalTime 类。在 StackOverflow 中搜索许多示例和讨论。

这是一个使用 LocalTime 的过于简单的示例,如果业务规则是“如果当前时间在 07:30:00.000 之后,无论用户/计算机/设备现在碰巧发现自己的位置,请发出警报”:

Boolean alarmFired = Boolean.FALSE;
LocalTime alarm = new LocalTime( 7 , 30 );
// …
DateTimeZone zoneDefault = DateTimeZone.getDefault();  // Use the computer’s/device’s JVM’s current default time zone. May change at any moment.
if ( LocalTime.now( zoneDefault ).isAfter( alarm ) ) {
    DateTime whenAlarmFired = DateTime.now( DateTimeZone.UTC );
    alarmFired = Boolean.TRUE;
    // fire the alarm.
}

使用 UTC 记录历史

请注意,如果我们正在跟踪警报触发时间的历史记录,我们应该在 UTC 中以DateTime. 我们可能还想知道警报在当地时间什么时间触发。如果我们知道或记录时区,我们总是可以重新创建它。或者我们也可以选择记录当前的本地日期时间。但是,本地价值是次要信息。主要跟踪应按 UTC。

明确指定时区

请注意我们如何将时区明确指定为默认时区。使用它会更短,LocalTime.now()因为它确实使用了 JVM 当前的默认时区。

我们正在谈论这之间的区别......</p>

LocalTime now = LocalTime.now();  // Implicit reliance on JVM’s current default time zone.

……还有这个……</p>

LocalTime now = LocalTime.now( DateTimeZone.getDefault() ); // Explicitly asking for JVM’s current default time zone.

隐式依赖默认时区是一种不好的做法。一方面,这种依赖鼓励程序员在本地日期时间参考框架中思考,而她应该养成以 UTC 思考的习惯。另一个问题是,隐式依赖默认时区会使您的代码模棱两可,因为读者不确定您是打算使用默认时区还是只是不知道更好(这通常是日期问题的原因-时间处理)。

于 2015-01-12T18:52:11.240 回答