如何手动使用 springs 默认对象映射器/手动将 Http-Request Body 反序列化为对象

How to manually use springs default object mapper / manually deserializing of Http-Request Body to Object

我有一个通用的 Rest-Constroller,它需要从实体名称和表示实体属性的请求主体中构造一个实体。 我需要手动将请求正文映射到目标 class.

我已经尝试了不同的方法来自定义我的 RestController 反序列化数据的方式,并且认为我当前的方法是最简单的。 我现在注册一个 @PostMapping 并检索我用来确定正确 class 的 entityName 和一个 @RequestBody String entityJson,我通过 ObjectMapper 将其转换为确定的 class.

但我在 LocalDate 反序列化过程中遇到问题(注意:我的日期字符串格式为 'yyyy-MM-dd'T'HH:mm:ss.SSSZ',其中时间为零)。

我将 springboot 2.1.6.RELEASE 与 jpa 2.1.9 一起使用,因此如果我使用 @RequestBody XXXEntity 实体,则不需要自定义处理 RestControllers 中的 LocalDate/LocalDateTime 属性范围。 但是,当将 OnjectMapper bean 注入我的 RestController 并尝试 objectMapper.readValue(entityJson, entityClass) 反序列化在 LocalDate 属性上失败时。

如果预先知道实体 class,我如何反序列化 spring 的方式(正确)?

1.) 没有自定义配置的异常: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:无法构造 java.time.LocalDate 的实例(不存在像默认构造那样的创建者):没有字符串参数 constructor/factory 方法可从字符串值反序列化 ('2019-10-29T00:00 :00.000Z')

2.) 使用自定义 JSR310Module ObjecktMapper 时

java.time.format.DateTimeParseException:无法解析文本“2019-10-29T00:00:00.000Z”,在索引 10

处找到未解析的文本
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
                .serializationInclusion(JsonInclude.Include.NON_NULL)
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 
                .modules(new JSR310Module())
                .build();
Same error with custom DateFormat:
  SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        objectMapper.setDateFormat(df);

我尝试为 LocalDate das 注册 LocalDateTime Deserializer:

 Jackson2ObjectMapperBuilder.json()
    .deserializerByType(LocalDate.class, LocalDateTimeDeserializer.INSTANCE)

结果变得更好...所以在我到达 UTC 时区时配对失败: java.time.format.DateTimeParseException:无法解析文本“2019-10-29T00:00:00.000Z”,在索引 23 处找到未解析的文本

这是我的控制器的样子:

    @Autowired
    public AdminRestController(EntityService entityService, ObjectMapper objectMapper) {
        this.entityService = entityService;
        this.objectMapper = objectMapper;
    }

...

    @Transactional
    @PostMapping(value = "/entities/{entityName}")
    public List<T> createEntity(@PathVariable String entityName, @RequestBody String entityJson) {
        T newEntity = constructEntity(entityName, entityJson);
        entityService.create(newEntity);
        ...
    }
...
    private T constructEntity(String entityName, String entityJson) {
        Class<T> entityClass;
        try {
            entityClass = findClassByName(entityName);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("Entity " + entityName + " unknwon.");
        }
        try {
            T entity = objectMapper.readValue(entityJson, entityClass);
            return entity;
        } catch (IOException e) {
            throw new IllegalStateException("Unable to construct entity  " + entityName + " from provided data.", e);
        }
    }

我有一个 "solution" 来解决我的问题...但是它很老套...

我将我的问题追查到一个根源: - 我的客户总是将 timezoned-utc-strings 作为日期发送,例如'2019-10-29T00:00:00.000Z'(也包括时间信息)而不是 "real" 日期字符串。 - 奇怪的是 springboot 默认情况下(至少使用 spring-boot-starter-web 等)如果这样的字符串在序列化时命中 LocalDate 属性,则可以正确处理此问题。 - 如果我尝试使用 jacksons ObjectMapper 手动重建此行为,我将无法同时解决这两个问题,并且会在时间或时区信息上遇到错误。

有什么帮助,例如正在使用自定义解串器注释我的 Pojo:

    @JsonDeserialize(using = LocalDateTimeStringToLocalDateDeserializer.class)
    private LocalDate validFrom;
/*
 * Copyright 2013 FasterXML.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the license for the specific language governing permissions and
 * limitations under the license.
 */

package ch.acrevison.tpl.findata.util.serializing;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Deserializer for Java 8 temporals
 * 1.) parses string as LocalDateTime if it is longer as an expected iso date.
 * 1.a ) if utc timezone marker is contained>
 */
public class LocalDateTimeStringToLocalDateDeserializer extends JsonDeserializer<LocalDate> implements java.io.Serializable {

    private static ZoneId ZONE_SWISS = ZoneId.of("Europe/Zurich");

    @Override
    public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException {

        String string = parser.getText().trim();
        if (string.length() == 0) {
            return null;
        }

        if (string.endsWith("Z")) {
            ZonedDateTime utcZonedDateTime = ZonedDateTime.parse(string);
            ZonedDateTime localDateTime = utcZonedDateTime.withZoneSameInstant(ZONE_SWISS);
            return localDateTime.toLocalDate();
        } else if (string.length() > 10) {
            return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        } else {
            return LocalDate.parse(string);
        }
    }
}

没有更好的想法?

您可能会认为它很老套。我的逻辑版本是:

    if (string.isEmpty()) {
        return null;
    }
    if (string.endsWith("Z")) {
        return Instant.parse(string).atZone(ZONE_SWISS).toLocalDate();
    }
    return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);

我已经避免了字符串的修改和从LocalDateTimeLocalDate的转换。此外,由于 Z 只能出现在字符串的最后,我的版本提供了更精确的验证。最后,作为品味问题,我喜欢 String.isEmpty 方法。