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

Java: Setting the timezone from within an OSGi application

提问人:Philipp 提问时间:1/12/2015 最后编辑:Philipp 更新时间:1/14/2015 访问量:834

问:

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

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

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

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

编辑:为了摆脱更新的需要,我可以使用一个始终检查默认时区的时钟,但从性能 POV 来看,这似乎不是最佳的: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

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

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

// 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);
时区 osgi java-time threetenbp

评论

0赞 Neil Bartlett 1/12/2015
什么?它是来自您自己的应用程序的类型,还是来自外部库的类型?Clock
1赞 Philipp 1/12/2015
Clock是 Java 8 Time API / threetenbp 的 s 提供方Instant
0赞 Basil Bourque 1/13/2015
@Philipp 正如我在回答中所说,你似乎是在用鼹鼠山造一座山。与桌面或Web应用程序相比,“嵌入式”或OSGi是否有什么特别或不同之处使情况复杂化?您不能创建和查找表示用户配置文件(例如其本地化语言(法语或意大利语)和首选时区)的对象集合吗?也许我对OSGi太无知了,无法发表评论。

答:

3赞 Neil Bartlett 1/12/2015 #1

根据我的经验,日期/时间应始终使用 UTC 在内部表示。只有在向用户显示时才会转换为本地时间。此时,您还需要根据用户的区域设置对其进行格式化。

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

更新

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

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

评论

0赞 Philipp 1/12/2015
如果我使用 ConfigAdmin,则所有使用 time 的捆绑包都将依赖于 ConfigAdmin 和特定的 PID。此外,每个都必须调用,以便首先启动的任何人设置默认值(例如,用于日志记录)。这是推荐的方式吗?TZ.setDefault()
0赞 Meno Hochschild 1/13/2015
@Philipp 答案是合理的。您还可以向 (web) 用户询问其时区并将其存储在会话和数据库中。顺便说一句,永远不要使用 更改服务器的默认时区。这只会更改所有用户的 default-tz(对服务器毫无意义甚至有害)。时钟将主要产生 UTC 偏移的瞬间。如果您想要“本地”日期,则只需使用用户的首选时区进行从 到 的转换,而不是默认的 tz。TimeZone.setDefault()InstantLocalDate
0赞 Basil Bourque 1/13/2015
强调:调用会影响在该 JVM 中运行的所有应用程序的所有线程中的所有代码。一般来说,这不是一件好事。TimeZone.setDefault
0赞 Philipp 1/13/2015
实际上,我的代码在嵌入式设备上运行,并使用本地时间以及时区来做出控制决策。因此,全局设置JVM时区确实是我所需要的。
0赞 Basil Bourque 1/13/2015 #2

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

像本地化文本一样处理日期时间值

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

使用一种语言(例如英语)执行所有内部工作,例如查找和键值,即使没有用户会说英语。

对于日期时间,等效的是将内部值保留在 UTC 时区。业务逻辑、文件存储和数据库记录都应使用 UTC。如果了解原始时区和/或原始输入至关重要,请将其存储为 UTC 调整值之外的附加信息。

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

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

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

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

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

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

登录UTC

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

不要惹Clock

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

例如,假设用户希望在早上 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éalalarmUtc

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

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,那么您将使用 ,这只是任何时区的早上 7-30 的想法。A与宇宙历史的时间线无关。LocalTimeLocalTime

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

下面是一个过于简单的示例,如果业务规则是“如果当前时间在用户/计算机/设备碰巧发现自己现在所在的任何位置的 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 中作为 .我们可能还想知道警报是在当地时间什么时间发出的。如果我们知道或记录了时区,我们总是可以重新创建它。或者,我们也可以选择记录当前的本地日期时间。但该本地值是次要信息。主要跟踪应按 UTC 进行。DateTime

显式指定时区

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

我们正在谈论这两者之间的区别......

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.

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

评论

0赞 Philipp 1/13/2015
谢谢你的回答。我的业务逻辑取决于时区(例如,7:30 的闹钟),所以这不仅仅是一个显示/本地化问题。尽管如此,我还是取消了您的建议,即不要触及 JVM 时区,而是在每个需求中使用用户指定的 TZ。
0赞 Basil Bourque 1/14/2015
@Philipp 不,我认为这确实是同一种问题。我在答案的底部添加了两个示例。其中任何一个都适用于你的应用吗?如果没有,请编辑您的问题,以解释您所说的“我的业务逻辑取决于时区”的意思。
0赞 Philipp 1/14/2015
典型的业务规则决策是“当前是否在 7:30 之后”。我会将其实现为 .对 now() 的调用使用时区。也许我可以将所有这些转换回 UTC,但随后它变得非常不可读 IMO。LocalTime.now(clock).isAfter(LocalTime.of(7,30))
0赞 Basil Bourque 1/14/2015
@Philipp 跟踪不是全有或全无的问题。当您指的是任何(或所有)地点的一天中的时间时,使用是正确的。这意味着用户/计算机/设备碰巧发现自己位于哪个时区。这样的本地时间与时间线无关,因此在UTC中没有意义。请注意,您可以将 a 应用于 a 以使用 LocalTime::toDateTimeToday( DateTimeZone zone ) (Joda-Time) 等方法在时间轴上创建一个点。LocalTimeLocalTimeDateTime
0赞 Basil Bourque 1/14/2015
@Philipp P.S. 对不起,所有的 Joda-Time 代码而不是 java.time。我只是对java.time不太熟悉。我敢肯定,对于我们讨论的材料,您会发现从 Joda-Time 到 java.time 的几乎直接翻译。附言请参阅我的答案底部的其他示例代码和讨论。[上面的评论旨在开始“按UTC跟踪......”。