比较 Java 日期并将日期强制为特定时区

Comparing Java Dates and force a date to a particular time zone

我在比较日期时遇到问题。

我正在使用 Groovy 和 Spock 编写针对 Web 服务的集成测试。

测试是首先使用网络服务创建一个Thing,然后立即再次调用以通过其ID 获取Thing 的详细信息。然后我想验证这个东西的 CreatedDate 是否大于一分钟前。

这里是一些JSON的调用

{
  "Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d",
  "CreatedDate":"2017-07-11T10:53:52"
}

因此,请注意日期字符串中没有时区信息;但我知道它是 UTC。

我是 Java(来自 .NET)的新手,对不同的日期类型有点迷惑。

这是我使用 Gson 反序列化的 class 的 Groovy 模型:

class Thing {
    public UUID Id
    public Date CreatedDate
}

反序列化工作正常。但是在非 UTC 时区运行的代码认为日期实际上是在本地时区。

我可以使用 Instant class:

创建一个代表“1 分钟前”的变量
def aMinuteAgo = Instant.now().plusSeconds(-60)

这就是我尝试进行比较的方式:

rule.CreatedDate.toInstant().compareTo(aMinuteAgo) < 0

麻烦的是,运行时认为日期是当地时间。我似乎没有过载强制 .toInstant() 进入 UTC。

我尝试使用我认为更现代的 classes - 例如我模型中的 LocalDateTimeZonedDateTime 而不是 Date,但是 Gson反序列化效果不佳。

输入的String只有日期和时间,没有时区信息,所以可以解析成LocalDateTime再转成UTC:

// parse date and time
LocalDateTime d = LocalDateTime.parse("2017-07-11T10:53:52");
// convert to UTC
ZonedDateTime z = d.atZone(ZoneOffset.UTC);
// or
OffsetDateTime odt = d.atOffset(ZoneOffset.UTC);
// convert to Instant
Instant instant = z.toInstant();

您可以使用 ZonedDateTimeOffsetDateTimeInstant,因为它们都将包含等效的 UTC 日期和时间。要获得它们,您可以使用解串器 .

要检查此日期和当前日期之间的分钟数,您可以使用 java.time.temporal.ChronoUnit:

ChronoUnit.MINUTES.between(instant, Instant.now());

这将 return instant 和当前 date/time 之间的分钟数。您还可以将其与 ZonedDateTimeOffsetDateTime:

一起使用
ChronoUnit.MINUTES.between(z, ZonedDateTime.now());
ChronoUnit.MINUTES.between(odt, OffsetDateTime.now());

但是当你使用 UTC 时,Instant 更好(因为根据定义它是 "in UTC" - 实际上,Instant 代表一个时间点,自 epoch (1970-01-01T00:00Z) 以来的纳秒并且没有 timezone/offset,所以你也可以认为 "it's always in UTC").

如果您确定日期始终采用 UTC,也可以使用 OffsetDateTimeZonedDateTime 也可以,但是如果您不需要时区规则(跟踪 DST 规则等),那么 OffsetDateTime 是更好的选择。

另一个区别是Instant只有自纪元1970-01-01T00:00Z)以来的纳秒数。如果需要日、月、年、时、分、秒等字段,最好使用ZonedDateTimeOffsetDateTime.


你也可以看看 API tutorial, which has a good explanation about the different types.

非常感谢您的评论,他们让我走上了一条好的道路。

为了其他遇到此问题的人的利益,这是我使用的代码。

修改模型class:

import java.time.LocalDateTime

class Thing {
   public UUID Id
   public LocalDateTime CreatedDate
}

Utils class(另外还包括 ZonedDateTime 的方法,因为那是我获得原始代码的地方。结果我可以让 LocalDateTime 为我工作。( setDateFormat 是为了支持 Date 在其他模型 class 中使用的对象,我不需要进行比较,尽管我可以看到自己很快就会弃用所有这些)。

class Utils {
    static Gson UtilGson = new GsonBuilder()
            .registerTypeAdapter(ZonedDateTime.class, GsonHelper.ZDT_DESERIALIZER)
            .registerTypeAdapter(LocalDateTime.class, GsonHelper.LDT_DESERIALIZER)
            .registerTypeAdapter(OffsetDateTime.class, GsonHelper.ODT_DESERIALIZER)
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
            .create();

    // From 
    static class GsonHelper {

        public static final JsonDeserializer<ZonedDateTime> ZDT_DESERIALIZER = new JsonDeserializer<ZonedDateTime>() {
            @Override
            public ZonedDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
                JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
                try {

                    // if provided as String - '2011-12-03T10:15:30+01:00[Europe/Paris]'
                    if(jsonPrimitive.isString()){
                        return ZonedDateTime.parse(jsonPrimitive.getAsString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
                    }

                    // if provided as Long
                    if(jsonPrimitive.isNumber()){
                        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(jsonPrimitive.getAsLong()), ZoneId.systemDefault());
                    }

                } catch(RuntimeException e){
                    throw new JsonParseException("Unable to parse ZonedDateTime", e);
                }
                throw new JsonParseException("Unable to parse ZonedDateTime");
            }
        };

        public static final JsonDeserializer<LocalDateTime> LDT_DESERIALIZER = new JsonDeserializer<LocalDateTime>() {
            @Override
            public LocalDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
                JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
                try {

                    // if provided as String - '2011-12-03T10:15:30'
                    if(jsonPrimitive.isString()){
                        return LocalDateTime.parse(jsonPrimitive.getAsString(), DateTimeFormatter.ISO_DATE_TIME);
                    }

                    // if provided as Long
                    if(jsonPrimitive.isNumber()){
                        return LocalDateTime.ofInstant(Instant.ofEpochMilli(jsonPrimitive.getAsLong()), ZoneId.systemDefault());
                    }

                } catch(RuntimeException e){
                    throw new JsonParseException("Unable to parse LocalDateTime", e);
                }
                throw new JsonParseException("Unable to parse LocalDateTime");
            }

         public static final JsonDeserializer<OffsetDateTime> ODT_DESERIALIZER = new JsonDeserializer<OffsetDateTime>() {
        @Override
        public OffsetDateTime deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
            JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive()
            try {

                // if provided as String - '2011-12-03T10:15:30' (i.e. no timezone information e.g. '2011-12-03T10:15:30+01:00[Europe/Paris]')
                // We know our services return UTC dates without specific timezone information so can do this.
                // But if, in future we have a different requirement, we'll have to review.
                if(jsonPrimitive.isString()){
                    LocalDateTime localDateTime = LocalDateTime.parse(jsonPrimitive.getAsString());
                    return localDateTime.atOffset(ZoneOffset.UTC)
                }
            } catch(RuntimeException e){
                throw new JsonParseException("Unable to parse OffsetDateTime", e)
            }
            throw new JsonParseException("Unable to parse OffsetDateTime")
        }
    }
        };
    }

这里是进行比较的代码(Spock/Groovy):

// ... first get the JSON text from the REST call

when:
text = response.responseBody
def thing = Utils.UtilGson.fromJson(text, Thing.class)
def now = OffsetDateTime.now(ZoneOffset.UTC)
def aMinuteAgo = now.plusSeconds(-60)

then:
thing.CreatedDate > aMinuteAgo
thing.CreatedDate < now

用运算符做比较似乎更自然。当我明确将它用于 UTC 时,OffsetDateTime 效果很好。我只使用此模型对服务(它们本身实际上是在 .NET 中实现的)执行集成测试,因此不会在我的测试之外使用这些对象。