Java:在 OSGi 应用程序中设置时区

Java: Setting the timezone from within an OSGi application

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

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

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

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

编辑:为了摆脱更新 Clock 的需要,我可以使用一个始终检查默认时区的时钟,但这似乎不是最佳选择表演视角:

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 - 这与使用我上面的 DefaultZoneClock 完全相同,不同之处在于使用我的代码我可以注入任何其他 Clock 进行测试。 (所有客户端代码都将使用 LocalDate.now(clock)

下面的答案建议不要更改 JVM 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);

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

那么问题就变成了,你怎么知道用户的时区和地区?这取决于应用程序的性质。如果它是 single-user 桌面应用程序,那么您应该在 OS 中查找这些应用程序或使用与 Config Admin 服务一起存储的配置。如果它是一个 multi-user 网络应用程序,那么您可能会在网络 session 中获得一些 user-related 信息;或者使用 Accept-Language header 甚至 geo-location.

更新

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

我认为您不应该从 Java 应用程序调用 TimeZone.setDefault,因为您要从哪里获得这些信息?也许直接用户输入,但用户不会更喜欢在 OS 中设置它,以便所有应用程序都能获得它吗?

你好像小题大做了,努力把一个简单的问题搞成一个复杂的问题。

像本地化文本一样对待 Date-Time 值

本地化 date-time 是一个本地化问题。如果某些用户说法语、一些意大利语和一些德语,您可以采用类似的方式处理它。

即使 none 的用户说英语,也可以使用一种语言(比如英语)完成所有内部工作,例如查找和键值。

对于 date-time,等效于将内部值保留在 UTC 时区中。您的业​​务逻辑、文件存储和数据库记录都应该使用 UTC。如果知道原始时区 and/or 原始输入至关重要,请将其存储为 UTC-adjusted 值之外的附加信息。

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

  • 用户计算环境的默认语言(JVM 默认,http headers,Java脚本查询,随便什么)
  • 用户明确选择的语言(我们的应用询问用户)
  • 适合数据上下文的语言。

对于前两个,您在某个地方以某种方式为每个用户保留了一个配置文件。在网络应用程序中,我们使用 session object。在 single-user "desktop" 应用程序中,我们使用单例。对于用户选择的语言,保存到数据库或文件存储中,以便记住该用户将来的工作session。

date-time也是如此。在呈现或导出数据时,如果上下文没有规定时区,则从他们的计算环境中检测他们当前的时区。如果时区很重要,或者用户流动性很强并且跨越时区,则请用户选择时区。提供 proper time zone names 的列表。记住此选择,以便此用户 session 以后的工作。

指定时区 Date-Time Objects

Joda-Time 库和 Java 8 中的 java.time 包都可以轻松调整 date-time object 的指定时区和来自 UTC。您应该在此处根据数据值 objects 更改时区,而不是更改 JVM 的默认值或 OS 语言环境设置。

以 UTC 登录

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

不要惹 Clock

在生产中部署时,我不会弄乱 java.time Clock。 class 的主要目的是测试。您可以插入一个伪造的时钟来谎报当前 date-time 以创建测试场景。您 可以 在生产中插入一个时钟,但对我来说,在 date-time object 上指定所需的时区更有意义。

例子

例如,假设用户想要在 7:30 AM 触发警报。您要么询问用户,要么确定他们想要的时区在蒙特利尔。这是 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。两者都代表宇宙历史时间轴上的同一时刻。

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

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 objects,都代表宇宙时间轴中的时刻:

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 在用户碰巧所在的任何本地时区,那么您将使用 LocalTime,这只是 [=117] 的想法=] 在任何时区的早上。 A LocalTime 与宇宙历史的时间轴无关。

Joda-Time 和 java.time 都提供本地时间 class。在 Whosebug 中搜索许多示例和讨论。

这是一个 overly-simplistic 示例,如果业务规则是 "Fire the alarm if the current time is after 07:30:00.000 in whatever locality the user/computer/device happens to find itself now":

,则使用 LocalTime
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 进行此操作。我们可能还想知道警报是在当地时间什么时候响起的。如果我们知道或记录时区,我们总是可以 re-create 那个。或者我们可以选择同时记录当前本地date-time。但该本地值是次要信息。主要跟踪应为 UTC。

明确指定时区

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

我们说的是这两者的区别……

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

……还有这个……

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

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