java - GMT0 的区域解析
问题描述
我将 GMT0 作为系统中的默认时区,当我对其进行序列化并在此之后对其进行反序列化时,它会导致问题。
System.setProperty("user.timezone","GMT0");
DateTimeFormatter zoneFormatter = new DateTimeFormatterBuilder()
.appendZoneOrOffsetId()
.toFormatter();
String formatted = zoneFormatter.format(ZonedDateTime.now());
System.out.println(formatted);
System.out.println(zoneFormatter.parse(formatted));
第一个System.out.println
打印GMT0
,而第二个打印出以下问题。
Exception in thread "main" java.time.format.DateTimeParseException: Text 'GMT0' could not be parsed, unparsed text found at index 3
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1952)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1777)
这是预期的行为吗?有没有办法以安全的方式做到这一点?
解决方案
正如您在评论中注意到的那样,这是JDK 8 中的一个错误,仅在 >= 9 版本中修复。
如果您使用的是 JDK 8 并且不能/不会升级它,那么有一个解决方法。您可以将“GMT”部分视为文字(文本"GMT"
本身)并将 0 视为偏移秒,使用相应的ChronoField
:
DateTimeFormatter zoneParser = new DateTimeFormatterBuilder()
// text "GMT"
.appendLiteral("GMT")
// offset seconds
.appendValue(ChronoField.OFFSET_SECONDS)
.toFormatter();
System.out.println(zoneParser.parse("GMT0"));
请记住,这仅适用于偏移量为零。对于任何其他值(例如“GMT2”或“GMT-2”),这将不起作用,因为它将值“2”和“-2”视为秒,但它们实际上表示“小时”。
如果您需要以这种格式解析所有偏移值(“GMTn”)
好吧,JDK 8 也不能处理一位数的偏移量,它总是需要一个信号,要么+
要么-
。所以“GMT2”和“GMT-2”不适用于当前的 API。
不过,还有一个更难的选择:创建自己的TemporalField
,代表“偏移时间”。有关如何执行此操作的所有详细信息都在文档中,我不确定是否所有方法都已正确实现 - 我只是确定isSupportedBy
,getFrom
并且adjustInto
,其他可能需要一些改进/调整:
public class OffsetHours implements TemporalField {
@Override
public TemporalUnit getBaseUnit() {
return ChronoUnit.HOURS;
}
@Override
public TemporalUnit getRangeUnit() {
return ChronoUnit.FOREVER;
}
@Override
public ValueRange range() {
return ValueRange.of(ZoneOffset.MIN.getTotalSeconds() / 3600, ZoneOffset.MAX.getTotalSeconds() / 3600);
}
@Override
public boolean isDateBased() {
return false;
}
@Override
public boolean isTimeBased() {
return true;
}
@Override
public boolean isSupportedBy(TemporalAccessor temporal) {
return temporal.isSupported(ChronoField.OFFSET_SECONDS);
}
@Override
public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
ValueRange rangeInSecs = temporal.range(ChronoField.OFFSET_SECONDS);
return ValueRange.of(rangeInSecs.getMinimum() / 3600, rangeInSecs.getMaximum() / 3600);
}
@Override
public long getFrom(TemporalAccessor temporal) {
return temporal.getLong(ChronoField.OFFSET_SECONDS) / 3600;
}
@Override
public <R extends Temporal> R adjustInto(R temporal, long newValue) {
return (R) temporal.with(ChronoField.OFFSET_SECONDS, newValue * 3600);
}
}
现在您创建此字段的一个实例并在您的解析器中使用它:
// the new field
OffsetHours offsetHoursField = new OffsetHours();
DateTimeFormatter zoneParser = new DateTimeFormatterBuilder()
// text "GMT"
.appendLiteral("GMT")
// offset hours
.appendValue(offsetHoursField)
.toFormatter();
我还建议创建 aTemporalQuery
以将解析结果转换为 a ZoneOffset
:
// get hours and create offset from hours value
TemporalQuery<ZoneOffset> getOffsetFromHours = temporal -> {
return ZoneOffset.ofHours((int) temporal.getLong(offsetHoursField));
};
现在你可以解析它:
ZoneOffset offsetZero = zoneParser.parse("GMT0", getOffsetFromHours);
ZoneOffset offsetTwo = zoneParser.parse("GMT2", getOffsetFromHours);
ZoneOffset offsetMinusTwo = zoneParser.parse("GMT-2", getOffsetFromHours);
您可以改进它,让该OffsetHours
字段成为一个静态实例(或者可能是一个enum
),因此您不需要一直创建它。