【问题标题】:Comparing Java Dates and force a date to a particular time zone比较 Java 日期并将日期强制为特定时区
【发布时间】:2017-07-11 11:11:29
【问题描述】:

我无法进行日期比较。

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

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

这是调用的一些 JSON

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

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

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

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

class Thing {
    public UUID Id
    public Date CreatedDate
}

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

我可以使用Instant 类创建一个表示“1 分钟前”的变量:

def aMinuteAgo = Instant.now().plusSeconds(-60)

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

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

问题是,运行时认为日期是本地时间。对于我来说,强制 .toInstant() 进入 UTC 似乎没有任何负担。

我尝试在我的模型中使用我所理解的更现代的类 - 例如 LocalDateTimeZonedDateTime 而不是 Date,但是 Gson 在反序列化方面表现不佳。

【问题讨论】:

  • 你需要注册你自己的解串器,见stackoverflow.com/a/36418842/6509
  • 如果您对该数据源有任何影响,如果确实打算成为 UTC 中的某个时刻,请让他们在该日期时间字符串的末尾附加一个 ZZ 是 Zulu 的缩写,意思是 UTC。

标签: java date groovy datetime-parsing


【解决方案1】:

输入的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 日期和时间。要获取它们,您可以使用反序列化器 as linked in the comments

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

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

这将返回instant 和当前日期/时间之间的分钟数。您也可以将其与ZonedDateTimeOffsetDateTime 一起使用:

ChronoUnit.MINUTES.between(z, ZonedDateTime.now());
ChronoUnit.MINUTES.between(odt, OffsetDateTime.now());

但是当您使用 UTC 时,Instant 更好(因为根据定义它是“在 UTC 中” - 实际上,Instant 代表一个时间点,即自 epoch (1970-01-01T00:00Z) 并且没有时区/偏移量,因此您也可以认为“它始终采用 UTC”)。

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

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


您还可以查看API tutorial,其中有一个good explanation about the different types

【讨论】:

    【解决方案2】:

    非常感谢 cmets,他们让我走上了一条好路。

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

    修改模型类:

    import java.time.LocalDateTime
    
    class Thing {
       public UUID Id
       public LocalDateTime CreatedDate
    }
    

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

    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 https://stackoverflow.com/a/36418842/276036
        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 中实现)执行集成测试,因此这些对象不会在我的测试之外使用。

    【讨论】:

    • 关于命名: (a) Java 中的变量按照约定具有初始小写字母。所以idcreatedDate,(b) 名称createdDate 是模棱两可的,很容易与仅日期的LocalDate 值混淆。我建议类似“whenCreated”。
    • 当您知道该值用于偏移量或区域时,请勿在 LocalDateTime 中工作。你会故意丢弃有价值的信息。如果您确定该值适用于 UTC,则表示为具有指定偏移量 (ZoneOffset.UTC) 的 OffsetDateTime。研究 Hugo 的答案,这很好,只是他应该使用 OffsetDateTime 而不是 ZonedDateTime 来表示 UTC。
    • @BasilBourque 确实,我忘了提OffsetDateTime。我已经更新了我的答案,谢谢!
    • @BasilBourque 和@Hugo,非常感谢您的帮助。关于 createdDate 属性的命名,这是一个公平的评论,但这适用于服务端,我只希望我的本地测试模型能够反映该命名。
    • @BasilBourque 和@Hugo 关于样式,我来自 .NET 背景,所以这对我来说似乎更自然。但是,我想以正确的方式做事,所以注意到了你所说的。问题是服务(在 .NET 中实现)返回 PascalCase 中的属性。因此,Gson 默认情况下不会反序列化。虽然我后来发现我可以使用.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE),但我测试过的其他服务有不同的大小写(!)。我现在想避免单独的 Gson 用于单独的服务。不过我以后可能会改变主意!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-17
    • 2015-02-15
    • 1970-01-01
    • 1970-01-01
    • 2017-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多