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



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

在Java/OOSGi应用程序中持久设置时区的推荐方式是什么

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

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

编辑:为了摆脱更新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,它通过在Map中搜索来设置当前的ZoneID——这与使用上面的DefaultZoneClock完全相同,不同之处在于使用我的代码我可以注入任何其他Clock进行测试。(所有客户端代码将使用LocalDate.now(clock))

下面的答案建议不要更改JVM时区,而是在必要时基于用户定义的时区进行转换,这意味着我必须注意不要使用从Clock调用时区的java.time方法,例如,如果我需要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);

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

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

更新

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

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

你似乎小题大做,努力把一个简单的问题变成一个复杂的问题。

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

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

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

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

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

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

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

约会时间也是如此。在呈现或导出数据时,如果上下文没有指定时区,则从其计算环境中检测其当前时区。如果时区很关键,或者用户的流动性很强并且跨越时区,请询问用户对时区的选择。提供正确时区名称的列表。请记住此用户将来工作会话的此选项。

指定时区日期时间对象

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

登录UTC

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

请勿干扰Clock

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

示例

例如,假设用户希望在早上7:30发出警报。你可以询问用户,也可以确定他们想要的时区在蒙特利尔。这是JodaTime中不切实际的代码(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

LocalTime

如果在另一种情况下,您希望在用户所在的任何本地时区7:30,那么您将使用LocalTime,这只是任何时区早上7:30的想法。LocalTime与宇宙历史的时间线无关。

JodaTime和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当前的默认时区。

我们正在讨论这个…之间的区别

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思考的习惯。另一个问题是,隐含地依赖默认时区会使您的代码变得模糊,因为读者不确定您是否打算使用默认时区,或者只是不知道(这往往是日期时间处理问题的原因)。

相关内容

  • 没有找到相关文章

最新更新