Java:在 OSGi 应用程序中设置时区
Java: Setting the timezone from within an OSGi application
我在 Linux 上的嵌入式 Java 应用程序 运行 应该允许用户通过 GUI 更改时区。我的应用程序在 OSGI 容器中 运行(请参阅下文为什么我认为这是相关的)并且在使用新时区之前不需要重新启动。
从我的 Java/OSGi 应用程序持久 设置时区的推荐方法是什么?
我可以想到以下方法,我列出了一些优点和缺点。我错过了什么吗?推荐什么?:
- 在应用程序中,更改基础 OS 时区,另外对当前 运行 JVM 使用
TimeZone.setDefault(...)
并更新所有持有旧 TZ 的 Clock
实例(所以某种事件是必要的)。缺点:此方法依赖于 OS 并且级别很低,我也想保留 OS 时钟 UTC。优点:OS 负责存储 TZ,下次启动应用程序时 TZ 立即正确。
- 在应用程序中,更改用于启动的
-Duser.timezone=...
参数。缺点:非常丑陋,甚至更低级别,但允许在 UTC 中保留 OS 时钟,同时让应用程序以正确的 TZ 启动。还需要在更改时续订 Clock
个实例。
- 不要碰 OS,只使用
TimeZone.setDefault(...)
并在启动时调用它 early。这将需要一个单独的持久性(首选项)来保存它。同样,在当前 运行 JVM 中,所有引用旧 TZ 的 Clock
实例都需要在更改时更新(需要事件)。当 OSGi 容器中的 运行 时,捆绑包的启动顺序无法保证,因此我无法确定默认 TZ 在使用前是否已设置。我怎么能保证这一点?此外,JSR310 明确建议不要在时钟中使用 "default TZ"。
- 根本不使用 "default" 时区,使用单独的全局变量,并且在
Instant
和 LocalXXX
值之间的每次转换时,显式传递时区。这摆脱了需要一个事件来更新 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 处理中的问题)。
我在 Linux 上的嵌入式 Java 应用程序 运行 应该允许用户通过 GUI 更改时区。我的应用程序在 OSGI 容器中 运行(请参阅下文为什么我认为这是相关的)并且在使用新时区之前不需要重新启动。
从我的 Java/OSGi 应用程序持久 设置时区的推荐方法是什么?
我可以想到以下方法,我列出了一些优点和缺点。我错过了什么吗?推荐什么?:
- 在应用程序中,更改基础 OS 时区,另外对当前 运行 JVM 使用
TimeZone.setDefault(...)
并更新所有持有旧 TZ 的Clock
实例(所以某种事件是必要的)。缺点:此方法依赖于 OS 并且级别很低,我也想保留 OS 时钟 UTC。优点:OS 负责存储 TZ,下次启动应用程序时 TZ 立即正确。 - 在应用程序中,更改用于启动的
-Duser.timezone=...
参数。缺点:非常丑陋,甚至更低级别,但允许在 UTC 中保留 OS 时钟,同时让应用程序以正确的 TZ 启动。还需要在更改时续订Clock
个实例。 - 不要碰 OS,只使用
TimeZone.setDefault(...)
并在启动时调用它 early。这将需要一个单独的持久性(首选项)来保存它。同样,在当前 运行 JVM 中,所有引用旧 TZ 的Clock
实例都需要在更改时更新(需要事件)。当 OSGi 容器中的 运行 时,捆绑包的启动顺序无法保证,因此我无法确定默认 TZ 在使用前是否已设置。我怎么能保证这一点?此外,JSR310 明确建议不要在时钟中使用 "default TZ"。 - 根本不使用 "default" 时区,使用单独的全局变量,并且在
Instant
和LocalXXX
值之间的每次转换时,显式传递时区。这摆脱了需要一个事件来更新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":
,则使用 LocalTimeBoolean 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 处理中的问题)。