提问人:Philipp 提问时间:1/12/2015 最后编辑:Philipp 更新时间:1/14/2015 访问量:834
Java:在 OSGi 应用程序中设置时区
Java: Setting the timezone from within an OSGi application
问:
我在 Linux 上运行的嵌入式 Java 应用程序应该允许用户通过 GUI 更改时区。我的应用程序在OSGI容器中运行(请参阅下文,为什么我认为这是相关的),并且在使用新时区之前不需要重新启动。
从我的 Java/OSGi 应用程序持续设置时区的推荐方法是什么?
我能想到以下方法,我列出了一些优点和缺点。 我错过了什么吗?推荐什么?
- 在应用程序中,更改底层操作系统时区,另外用于当前正在运行的 JVM 并续订所有持有旧 TZ 的实例(因此需要某种事件)。缺点:这种方法依赖于操作系统并且级别相当低,我也想保持操作系统时钟UTC。优点:操作系统负责存储 TZ,TZ 在下次启动应用程序时立即正确。
TimeZone.setDefault(...)
Clock
- 在应用程序中,更改用于启动的参数。缺点:非常丑陋,甚至更低级别,但允许将操作系统时钟留在 UTC,同时让应用程序以正确的 TZ 启动。还需要在更改时续订实例。
-Duser.timezone=...
Clock
- 不要触摸操作系统,只在启动时尽早使用和调用它。这将需要单独的持久性(首选项)来保存它。同样,在当前运行的 JVM 中,所有引用旧 TZ 的实例都需要在更改时进行更新(needs 事件)。在 OSGi 容器中运行时,无法保证 bundle 的启动顺序,因此我无法确定默认 TZ 在使用之前是否已设置。我该如何保证这一点?此外,JSR310 明确建议不要在 Clock 中使用“默认 TZ”。
TimeZone.setDefault(...)
Clock
- 完全不使用“默认”时区,使用单独的全局变量,并在 和 值之间的每次转换时,显式传递时区。这样就不需要事件来更新实例。但是我们需要注意不要使用 ,因为这会使用时钟的 TZ(然后不再正确)。如何在OSGi中拥有这个全局变量?用?如何使代码正确运行,这是我无法控制的(例如,记录时间戳)?
Instant
LocalXXX
Clock
LocalDate.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()
SytemClock
ZoneID
DefaultZoneClock
Clock
LocalDate.now(clock)
)
下面的答案建议不要更改 JVM 时区,而是在必要时根据用户定义的时区进行转换,这意味着我必须注意不要使用从 中调用时区的 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);
答:
根据我的经验,日期/时间应始终使用 UTC 在内部表示。只有在向用户显示时才会转换为本地时间。此时,您还需要根据用户的区域设置对其进行格式化。
那么问题来了,你怎么知道用户的时区和区域设置?这取决于应用程序的性质。如果是单用户桌面应用,则应在操作系统中查找这些应用,或者使用与 Config Admin 服务一起存储的配置。如果它是一个多用户 Web 应用程序,那么您可能会在 Web 会话中有一些与用户相关的信息;或使用 Accept-Language 标头甚至地理位置。
更新
海报澄清了该应用程序在嵌入式设备上是单用户的。在这种情况下,每次需要显示时间时,查询当前系统时区不是更容易吗?这避免了令人讨厌的全局变量,并且还允许您的应用程序动态响应时区的变化,例如,如果用户将设备从一个地方带到另一个地方。
我不认为你应该从你的Java应用程序调用,因为你要从哪里获取这些信息?也许是直接用户输入,但用户难道不愿意在操作系统中设置它,以便所有应用程序都可以获得它吗?TimeZone.setDefault
评论
TZ.setDefault()
TimeZone.setDefault()
Instant
LocalDate
TimeZone.setDefault
你似乎在用鼹鼠山造一座山,努力把一个简单的问题变成一个复杂的问题。
像本地化文本一样处理日期时间值
本地化日期时间是一个本地化问题。以类似于某些用户说法语、意大利语和德语的方式处理它。
使用一种语言(例如英语)执行所有内部工作,例如查找和键值,即使没有用户会说英语。
对于日期时间,等效的是将内部值保留在 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é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,那么您将使用 ,这只是任何时区的早上 7-30 的想法。A与宇宙历史的时间线无关。LocalTime
LocalTime
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思考的习惯。另一个问题是,隐式依赖默认时区会使你的代码变得模棱两可,因为读者不确定你是想使用默认时区还是只是不知道更好(这通常是日期时间处理出现问题的原因)。
评论
LocalTime.now(clock).isAfter(LocalTime.of(7,30))
LocalTime::toDateTimeToday( DateTimeZone zone ) (
Joda-Time) 等方法在时间轴上创建一个点。LocalTime
LocalTime
DateTime
评论
Clock
Clock
是 Java 8 Time API / threetenbp 的 s 提供方Instant