比较Java日期并强制将日期设置为特定时区
我在进行日期比较时遇到困难 我正在使用Groovy和Spock编写针对web服务的集成测试 测试是首先使用web服务创建一个对象,然后立即进行另一次调用以通过其ID获取该对象的详细信息。然后,我想验证该对象的CreatedDate是否大于一分钟前 下面是调用的一些JSON比较Java日期并强制将日期设置为特定时区,java,date,groovy,datetime-parsing,Java,Date,Groovy,Datetime Parsing,我在进行日期比较时遇到困难 我正在使用Groovy和Spock编写针对web服务的集成测试 测试是首先使用web服务创建一个对象,然后立即进行另一次调用以通过其ID获取该对象的详细信息。然后,我想验证该对象的CreatedDate是否大于一分钟前 下面是调用的一些JSON { "Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d", "CreatedDate":"2017-07-11T10:53:52" } 因此,请注意日期字符串中没有时区信息;但我知道
{
"Id":"696fbd5f-5a0c-4209-8b21-ea7f77e3e09d",
"CreatedDate":"2017-07-11T10:53:52"
}
因此,请注意日期字符串中没有时区信息;但我知道是UTC
我刚从.NET开始接触Java,对不同的日期类型有些迷惑
这是我的Groovy类模型,我使用Gson进行反序列化:
class Thing {
public UUID Id
public Date CreatedDate
}
反序列化工作得很好。但在非UTC时区运行的代码认为日期实际上在本地时区
我可以使用Instant类创建一个表示1分钟前的变量:
这就是我试图进行比较的方式:
rule.CreatedDate.toInstant().compareTo(aMinuteAgo) < 0
问题是,运行时认为日期是本地时间。似乎没有过载,我无法强制将.toInstant强制转换为UTC
我已经尝试使用我理解为更现代的类,例如模型中的LocalDateTime和ZonedDateTime,而不是Date,但是Gson在反序列化方面做得不好。输入字符串只有日期和时间,没有时区信息,因此您可以将其解析为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();
您可以使用ZonedDateTime、OffsetDateTime或Instant,因为它们都包含UTC中的等效日期和时间。要获取它们,可以使用反序列化程序
要检查此日期与当前日期之间的分钟数,可以使用java.time.temporal.ChronoUnit:
这将返回即时和当前日期/时间之间的分钟数。您还可以将其与ZonedDateTime或OffsetDateTime一起使用:
但是,当你使用UTC时,瞬间更好,因为它的定义是UTC——实际上,瞬间代表一个时间点,自1970-01-01T00:00Z纪元以来的纳秒数,没有时区/偏移,所以你也可以认为它总是在UTC
如果您确定日期始终以UTC为单位,也可以使用OffsetDateTime。ZonedDateTime也可以工作,但是如果您不需要时区规则来跟踪DST规则等等,那么OffsetDateTime是一个更好的选择
另一个不同之处是,从1970-01-01T00:00Z时代开始,瞬间只有纳秒数。如果需要日、月、年、小时、分钟、秒等字段,最好使用ZonedDateTime或OffsetDateTime
您还可以查看,它有一个。输入字符串只有日期和时间,没有时区信息,因此您可以将其解析为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();
您可以使用ZonedDateTime、OffsetDateTime或Instant,因为它们都包含UTC中的等效日期和时间。要获取它们,可以使用反序列化程序
要检查此日期与当前日期之间的分钟数,可以使用java.time.temporal.ChronoUnit:
这将返回即时和当前日期/时间之间的分钟数。您还可以将其与ZonedDateTime或OffsetDateTime一起使用:
但是,当你使用UTC时,瞬间更好,因为它的定义是UTC——实际上,瞬间代表一个时间点,自1970-01-01T00:00Z纪元以来的纳秒数,没有时区/偏移,所以你也可以认为它总是在UTC
如果您确定日期始终以UTC为单位,也可以使用OffsetDateTime。ZonedDateTime也可以工作,但是如果您不需要时区规则来跟踪DST规则等等,那么OffsetDateTime是一个更好的选择
另一个不同之处是,从1970-01-01T00:00Z时代开始,瞬间只有纳秒数。如果需要日、月、年、小时、分钟、秒等字段,最好使用ZonedDateTime或OffsetDateTime
您也可以看看,它有一个。非常感谢您的评论,他们让我走上了一条好路 为了让其他遇到这个问题的人受益,下面是我使用的代码 修改模型类:
import java.time.LocalDateTime
class Thing {
public UUID Id
public LocalDateTime CreatedDate
}
Utils类还包括ZoneDateTime的方法,因为我从中获得了原始代码。结果证明我可以让LocalDateTime为我工作。setDateFormat是为了支持在其他模型类中使用的日期对象,在这些模型类中我不需要进行比较,尽管我可以看到自己很快就会反对所有这些
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
与运算符进行比较似乎更为自然。当我显式地将OffsetDateTime用于UTC时,它工作得很好。我只使用此模型对实际在.NET中实现的服务执行集成测试,因此对象不会在测试之外使用。
anks的评论太多了,他们让我走上了一条好路
为了让其他遇到这个问题的人受益,下面是我使用的代码
修改模型类:
import java.time.LocalDateTime
class Thing {
public UUID Id
public LocalDateTime CreatedDate
}
Utils类还包括ZoneDateTime的方法,因为我从中获得了原始代码。结果证明我可以让LocalDateTime为我工作。setDateFormat是为了支持在其他模型类中使用的日期对象,在这些模型类中我不需要进行比较,尽管我可以看到自己很快就会反对所有这些
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
与运算符进行比较似乎更为自然。当我显式地将OffsetDateTime用于UTC时,它工作得很好。我只使用此模型对实际在.NET中实现的服务执行集成测试,因此对象不会在我的测试之外使用。您需要注册自己的反序列化程序,看看您是否对该数据源有任何影响,让他们在日期时间字符串的末尾附加一个Z,如果它确实是UTC中的一个时刻。Z是Zulu的缩写,表示UTC。您需要注册自己的反序列化程序,查看您是否对该数据源有任何影响,如果确实希望在UTC中出现某个时刻,请让他们在该日期时间字符串的末尾附加一个Z。Z是Zulu的缩写,表示UTC。关于命名:Java中的变量按约定以小写字母开头。因此id和createdDate,b createdDate的名称是不明确的,很容易与仅限日期的LocalDate值混淆。我建议使用类似于“whenCreated”的方式。如果您知道该值用于偏移量或区域,则不要在LocalDateTime中使用。你会故意丢弃有价值的信息。如果您确定该值适用于UTC,则使用分配的偏移区域offset.UTC表示为OffsetDateTime。研究雨果的答案,这很好,只是他应该使用OffsetDateTime而不是UTC的ZonedDateTime。@BasilBourque事实上,我忘了提到OffsetDateTime。我已经更新了我的答案,谢谢@BasilBourque和@Hugo,非常感谢你的帮助。关于createdDate属性的命名,这是一个公平的评论,但这适用于服务端,我只希望测试的本地模型反映该命名。@BasilBourque和@Hugo关于样式,我来自.NET背景,所以这对我来说更自然。然而,我想用正确的方式做事,所以我已经注意到了你所说的。问题在于.NET中实现的服务返回PascalCase中的属性。因此,默认情况下,Gson不会进行反序列化。尽管我后来发现我可以使用.setFieldNamingPolicyFieldNamingPolicy.UPPER_CAMEL_案例,但我测试的其他服务的大小写却不同!。我希望现在避免为单独的服务使用单独的Gson。不过我以后可能会改变主意!关于命名:Java中的变量按约定以小写字母开头。因此id和createdDate,b createdDate的名称是不明确的,很容易与仅限日期的LocalDate值混淆。我建议使用类似于“whenCreated”的方式。如果您知道该值用于偏移量或区域,则不要在LocalDateTime中使用。你会故意丢弃有价值的信息。如果您确定该值适用于UTC,则使用分配的偏移区域offset.UTC表示为OffsetDateTime。研究雨果的答案,这很好,只是他应该使用OffsetDateTime而不是UTC的ZonedDateTime。@BasilBourque事实上,我忘了提到OffsetDateTime。我已经更新了我的答案,谢谢@BasilBourque和@Hugo,非常感谢你的帮助。关于createdDate属性的命名,这是一个公平的评论,但这适用于服务端,我只希望测试的本地模型反映该命名。@BasilBourque和@Hugo关于样式,我来自.NET背景,所以这对我来说更自然。然而,我想用正确的方式做事,所以我已经注意到了你所说的。问题在于.NET中实现的服务返回PascalCase中的属性。因此,默认情况下,Gson不会进行反序列化。尽管我后来发现我可以使用.setFieldNamingPolicyFieldNamingPolicy.UPPER_CAMEL_案例,但我测试的其他服务的大小写却不同!。我希望现在避免为单独的服务使用单独的Gson。不过我以后可能会改变主意!