为啥 OffsetDateTime 序列化/反序列化结果有差异?

Posted

技术标签:

【中文标题】为啥 OffsetDateTime 序列化/反序列化结果有差异?【英文标题】:Why OffsetDateTime serializations/deserialization results have difference?为什么 OffsetDateTime 序列化/反序列化结果有差异? 【发布时间】:2022-01-07 06:04:22 【问题描述】:

我有以下对象:

@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;

我将此对象作为休息控制器的响应发送:

@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() 匹配的对象。

【问题讨论】:

发布代码输出上述输出,以便我们可以重现。见minimal reproducible example 好吧,2021-10-21T23:59:59.999999999-18:00 与 UTC 的偏移量为 -18:00 小时,而 2021-10-22T17:59:59.999999999Z 与 UTC 中的 Instant 相同(Z 表示 Zulu / UTC)。两者之间的差异是 18 小时。 您在同一时间有两种不同的表示(偏移量) 问题需要包括您编写的将这个东西序列化为 JSON 的代码(大概,当您用它标记它时,使用 Jackson),以及您如何反序列化它。 您有日期和时间2021-10-21T23:59:59.999999999,您将其定义为偏移-18:00(由atTime(OffsetTime.MAX)。这意味着您基本上必须 添加 18 小时才能在 UTC 中获得同一时刻的表示(偏移量为 +00:00 或只是 Z),这会导致不同的一天,因为午夜前的时刻增加了 18 小时,这将变成第二天的时间。 @OleV.V.或者我们可以关闭并删除代码示例不一致的不完整问题。 【参考方案1】:

到目前为止我们所知道的

    您正在从 LocalDate 创建一个 OffsetDateTime,并添加可用的最大偏移量(恰好是 -18:00 小时) 这个OffsetDateTime被正确序列化为2021-10-21T23:59:59.999999999-18:00的JSON值 反序列化时,值(如String)为2021-10-22T17:59:59.999999999Z

目前还没有包括关键部分:2. 和 3. 之间会发生什么? 请考虑用您所知道的一切更新您的问题。

我们可以得出什么

出现不一致的值基本上是同一时刻 (Instant),但在序列化时以 -18:00 的偏移量表示,并以 UTC 表示 (+00:00 或简单的 Z)。由于这些时刻之间有 18 小时的差异,并且由于您使用 OffsetTime.MAX(即 23:59:59.999999999-18:00,一天中的最大时间偏移为 -18:00)创建了一个 OffsetDateTime

这就是为什么反序列化后得到的结果没有错,但它的表示可能不是你想要的。

我的猜测是Instant 用于 2. 和 3. 之间的子步骤,并且反序列化仅提供 UTC 格式的日期和时间。

如果没有明确要求,我不会将最大偏移量传递给任何 API。是你的情况吗?也可以考虑添加相关信息。

我们可以做些什么来让Strings 的时间相等

您可以使用与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)并且反序列化应该返回相同的值。


如果您在 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 实际上那个时刻。 这意味着您应该及时比较真实的时刻,而不是基于上下文的表示。

【讨论】:

我添加了一些细节。请再读一遍我的问题好吗? @Zhenyria 请查看我的编辑。

以上是关于为啥 OffsetDateTime 序列化/反序列化结果有差异?的主要内容,如果未能解决你的问题,请参考以下文章

我无法将 '2017-04-04T08:04+0000' 解析为 OffsetDateTime

在 Spring REST Controller 中对 DateTime 使用 Jackson 反序列化

Jackson OffsetDateTime 序列化 Z 而不是 +00:00 时区?

无法反序列化当前的JSON对象,为啥

反序列化为啥要找公开方法

为啥我在 XML 反序列化函数中收到此错误?