首页 > 解决方案 > 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)

这是预期的行为吗?有没有办法以安全的方式做到这一点?

标签: javajava-time

解决方案


正如您在评论中注意到的那样,这是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,代表“偏移时间”。有关如何执行此操作的所有详细信息都在文档中,我不确定是否所有方法都已正确实现 - 我只是确定isSupportedBygetFrom并且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),因此您不需要一直创建它。


推荐阅读