为什么 OffsetDateTime serializations/deserialization 结果有差异?
Why OffsetDateTime serializations/deserialization results have difference?
我有以下对象:
@Validated
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
@Schema(description = "Request")
public final class Request implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("date")
@Schema(description = "Date")
private OffsetDateTime date;
}
然后我将此对象作为 rest-controller 的响应发送:
@RestController
public class RequestController {
@RequestMapping(
value = "/requests",
produces = {"application/json;charset=UTF-8"},
consumes = {"application/json"},
method = RequestMethod.POST)
public ResponseEntity<Request> get() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
return ResponseEntity.ok(request);
}
}
但我有配置:
@Configuration
public class WebConfiguration implements ServletContextInitializer, WebMvcConfigurer {
private final List<FilterRegistration> filterRegistrations;
private final ApplicationContext applicationContext;
public WebConfiguration(List<RestApplicationInstaller> restApplicationInstallers,
List<MonitoringRestApplicationInstaller> monitoringRestApplicationInstallers,
List<FilterRegistration> filterRegistrations,
ApplicationContext applicationContext) {
this.filterRegistrations = filterRegistrations;
this.applicationContext = applicationContext;
}
@Override
public void onStartup(ServletContext servletContext) {
VersionServletInstaller.installServlets(servletContext, getRegisterAsyncService(servletContext));
filterRegistrations.forEach(filterRegistration -> filterRegistration.onApplicationEvent(new ContextRefreshedEvent(applicationContext)));
}
private RegisterAsyncService getRegisterAsyncService(final ServletContext servletContext) {
final WebApplicationContext ctx = getWebApplicationContext(servletContext);
final RegisterAsyncService registerAsyncService = Objects.requireNonNull(ctx).getBean(RegisterAsyncService.class);
registerAsyncService.exec();
return registerAsyncService;
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer(CustomAnnotationIntrospector customAnnotationIntrospector) {
return builder -> builder.serializationInclusion(NON_NULL)
.annotationIntrospector(customAnnotationIntrospector);
}
}
好的。
所以...我得到 date
字段作为响应:
2021-10-21T23:59:59.999999999-18:00
当我测试我的控制器时,我尝试获得响应,将其反序列化为 Request
对象并检查匹配:
@DirtiesContext
@SpringBootTest(
classes = {WebConfiguration.class, JacksonAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
@EnableWebMvc
class RequestControllerTest {
private static final CharacterEncodingFilter
CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
static {
CHARACTER_ENCODING_FILTER.setEncoding(DEFAULT_ENCODING);
CHARACTER_ENCODING_FILTER.setForceEncoding(true);
}
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
@PostConstruct
private void postConstruct() {
this.mockMvc =
MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(CHARACTER_ENCODING_FILTER)
.build();
}
@Test
void requestByIdTest() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.post("/requests")
.characterEncoding(CHARACTER_ENCODING_FILTER)
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(
result -> Assertions.assertEquals(mapToObject(result.getResponse().getContentAsString(Charset.forName(CHARACTER_ENCODING_FILTER)), Request.class), getExpectedRequest()));
}
private WebComplianceRequest getExpectedRequest() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
}
private <T> T mapToObject(String json, Class<T> targetClass) {
try {
return getReaderForClass(targetClass).readValue(json);
} catch (IOException e) {
throw new RuntimeExsception(e);
}
}
private <T> ObjectReader getReaderForClass(Class<T> targetClass) {
return objectMapper.readerFor(targetClass);
}
}
但我得到一个异常,因为 date
预期对象和得到的对象中的字段不同:
Date in response: 2021-10-22T17:59:59.999999999Z
Expected date: 2021-10-21T23:59:59.999999999-18:00
为什么会这样?
为什么显示 Z
而不是时区?为什么日期从 2021-10-21
更改为 2021-10-22
?我该如何解决?
我没有得到任何异常,我得到匹配失败,因为当我匹配响应和预期对象时日期不同。我只是用标准 ObjectMapper
反序列化对象并检查与 equals()
.
匹配的对象
到目前为止我们所知道的
- 您正在从
LocalDate
添加最大可用偏移量(恰好是 -18:00
小时)OffsetDateTime
- 此
OffsetDateTime
被正确序列化为 JSON 值 2021-10-21T23:59:59.999999999-18:00
- 反序列化后,值(如
String
)为2021-10-22T17:59:59.999999999Z
到目前为止还没有包括关键部分:2.和3.之间发生了什么?
请考虑用你所知道的一切更新你的问题。
我们可以得出什么
出现不一致的值基本上是同一时刻 (Instant
),但在序列化时以 -18:00
的偏移量表示并以 UTC 表示(+00:00
或简称Z
)。由于这些时刻之间相差 18 小时,并且由于您创建了 OffsetDateTime
和 OffsetTime.MAX
(即 23:59:59.999999999-18:00
,一天中的最长时间偏移量为 -18:00
).
这就是为什么你在反序列化后得到的结果没有错,但它的表示可能不是你想要的。
我的猜测是在 2. 和 3. 之间的子步骤中使用了 Instant
,反序列化仅提供 UTC 日期和时间。
如果没有明确要求,我不会将最大偏移量传递给任何 API。是你的情况吗?也考虑添加相关信息。
我们可以做些什么来使时间 String
s 相等
您可以使用从 LocalDate
创建 OffsetDateTime
的不同可能性,即使用一天中的最长时间,而没有在 UTC 明确偏移:
OffsetDateTime dateTime = OffsetDateTime.of(date, LocalTime.MAX, ZoneOffset.UTC);
这将序列化为 2021-10-21T23:59:59.999999999Z
,您也可以将其表示为 2021-10-21T23:59:59.999999999+00:00
或类似的(我会坚持使用 Z
)和反序列化 应该 return相同的值。
如果您在 UTC 中收到 String
表示并且您对其没有任何影响,则必须解析它并通过应用最小偏移量更改表示(-18:00
), 可能是这样的:
String atMinOffset = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z")
.withOffsetSameInstant(ZoneOffset.MIN)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
System.out.println(atMinOffset);
输出:
2021-10-21T23:59:59.999999999-18:00
如果您收到 OffsetDateTime
作为响应并且只想检查它是否是同一时间点,请考虑以下:
public static void main(String[] args) throws IOException {
OffsetDateTime utcOdt = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z");
OffsetDateTime minOffsetOdt = OffsetDateTime.parse("2021-10-21T23:59:59.999999999-18:00");
System.out.println("OffsetDateTimes equal? --> " + utcOdt.equals(minOffsetOdt));
System.out.println("Instants equal? --> " + utcOdt.toInstant().equals(minOffsetOdt.toInstant()));
}
它的输出是
OffsetDateTimes equal? --> false
Instants equal? --> true
为什么?
OffsetDateTime
是对某个时刻的 表示,而 Instant
实际上 是 那个时刻。
这意味着您应该比较真实的时刻而不是基于上下文的表示。
我有以下对象:
@Validated
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
@Schema(description = "Request")
public final class Request implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("date")
@Schema(description = "Date")
private OffsetDateTime date;
}
然后我将此对象作为 rest-controller 的响应发送:
@RestController
public class RequestController {
@RequestMapping(
value = "/requests",
produces = {"application/json;charset=UTF-8"},
consumes = {"application/json"},
method = RequestMethod.POST)
public ResponseEntity<Request> get() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
return ResponseEntity.ok(request);
}
}
但我有配置:
@Configuration
public class WebConfiguration implements ServletContextInitializer, WebMvcConfigurer {
private final List<FilterRegistration> filterRegistrations;
private final ApplicationContext applicationContext;
public WebConfiguration(List<RestApplicationInstaller> restApplicationInstallers,
List<MonitoringRestApplicationInstaller> monitoringRestApplicationInstallers,
List<FilterRegistration> filterRegistrations,
ApplicationContext applicationContext) {
this.filterRegistrations = filterRegistrations;
this.applicationContext = applicationContext;
}
@Override
public void onStartup(ServletContext servletContext) {
VersionServletInstaller.installServlets(servletContext, getRegisterAsyncService(servletContext));
filterRegistrations.forEach(filterRegistration -> filterRegistration.onApplicationEvent(new ContextRefreshedEvent(applicationContext)));
}
private RegisterAsyncService getRegisterAsyncService(final ServletContext servletContext) {
final WebApplicationContext ctx = getWebApplicationContext(servletContext);
final RegisterAsyncService registerAsyncService = Objects.requireNonNull(ctx).getBean(RegisterAsyncService.class);
registerAsyncService.exec();
return registerAsyncService;
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer(CustomAnnotationIntrospector customAnnotationIntrospector) {
return builder -> builder.serializationInclusion(NON_NULL)
.annotationIntrospector(customAnnotationIntrospector);
}
}
好的。
所以...我得到 date
字段作为响应:
2021-10-21T23:59:59.999999999-18:00
当我测试我的控制器时,我尝试获得响应,将其反序列化为 Request
对象并检查匹配:
@DirtiesContext
@SpringBootTest(
classes = {WebConfiguration.class, JacksonAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
@EnableWebMvc
class RequestControllerTest {
private static final CharacterEncodingFilter
CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
static {
CHARACTER_ENCODING_FILTER.setEncoding(DEFAULT_ENCODING);
CHARACTER_ENCODING_FILTER.setForceEncoding(true);
}
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
@PostConstruct
private void postConstruct() {
this.mockMvc =
MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(CHARACTER_ENCODING_FILTER)
.build();
}
@Test
void requestByIdTest() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.post("/requests")
.characterEncoding(CHARACTER_ENCODING_FILTER)
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(
result -> Assertions.assertEquals(mapToObject(result.getResponse().getContentAsString(Charset.forName(CHARACTER_ENCODING_FILTER)), Request.class), getExpectedRequest()));
}
private WebComplianceRequest getExpectedRequest() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
}
private <T> T mapToObject(String json, Class<T> targetClass) {
try {
return getReaderForClass(targetClass).readValue(json);
} catch (IOException e) {
throw new RuntimeExsception(e);
}
}
private <T> ObjectReader getReaderForClass(Class<T> targetClass) {
return objectMapper.readerFor(targetClass);
}
}
但我得到一个异常,因为 date
预期对象和得到的对象中的字段不同:
Date in response: 2021-10-22T17:59:59.999999999Z
Expected date: 2021-10-21T23:59:59.999999999-18:00
为什么会这样?
为什么显示 Z
而不是时区?为什么日期从 2021-10-21
更改为 2021-10-22
?我该如何解决?
我没有得到任何异常,我得到匹配失败,因为当我匹配响应和预期对象时日期不同。我只是用标准 ObjectMapper
反序列化对象并检查与 equals()
.
到目前为止我们所知道的
- 您正在从
LocalDate
添加最大可用偏移量(恰好是-18:00
小时)OffsetDateTime
- 此
OffsetDateTime
被正确序列化为 JSON 值2021-10-21T23:59:59.999999999-18:00
- 反序列化后,值(如
String
)为2021-10-22T17:59:59.999999999Z
到目前为止还没有包括关键部分:2.和3.之间发生了什么?
请考虑用你所知道的一切更新你的问题。
我们可以得出什么
出现不一致的值基本上是同一时刻 (Instant
),但在序列化时以 -18:00
的偏移量表示并以 UTC 表示(+00:00
或简称Z
)。由于这些时刻之间相差 18 小时,并且由于您创建了 OffsetDateTime
和 OffsetTime.MAX
(即 23:59:59.999999999-18:00
,一天中的最长时间偏移量为 -18:00
).
这就是为什么你在反序列化后得到的结果没有错,但它的表示可能不是你想要的。
我的猜测是在 2. 和 3. 之间的子步骤中使用了 Instant
,反序列化仅提供 UTC 日期和时间。
如果没有明确要求,我不会将最大偏移量传递给任何 API。是你的情况吗?也考虑添加相关信息。
我们可以做些什么来使时间 String
s 相等
您可以使用从 LocalDate
创建 OffsetDateTime
的不同可能性,即使用一天中的最长时间,而没有在 UTC 明确偏移:
OffsetDateTime dateTime = OffsetDateTime.of(date, LocalTime.MAX, ZoneOffset.UTC);
这将序列化为 2021-10-21T23:59:59.999999999Z
,您也可以将其表示为 2021-10-21T23:59:59.999999999+00:00
或类似的(我会坚持使用 Z
)和反序列化 应该 return相同的值。
如果您在 UTC 中收到 String
表示并且您对其没有任何影响,则必须解析它并通过应用最小偏移量更改表示(-18:00
), 可能是这样的:
String atMinOffset = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z")
.withOffsetSameInstant(ZoneOffset.MIN)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
System.out.println(atMinOffset);
输出:
2021-10-21T23:59:59.999999999-18:00
如果您收到 OffsetDateTime
作为响应并且只想检查它是否是同一时间点,请考虑以下:
public static void main(String[] args) throws IOException {
OffsetDateTime utcOdt = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z");
OffsetDateTime minOffsetOdt = OffsetDateTime.parse("2021-10-21T23:59:59.999999999-18:00");
System.out.println("OffsetDateTimes equal? --> " + utcOdt.equals(minOffsetOdt));
System.out.println("Instants equal? --> " + utcOdt.toInstant().equals(minOffsetOdt.toInstant()));
}
它的输出是
OffsetDateTimes equal? --> false
Instants equal? --> true
为什么?
OffsetDateTime
是对某个时刻的 表示,而 Instant
实际上 是 那个时刻。
这意味着您应该比较真实的时刻而不是基于上下文的表示。