如何手动使用 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);
我已经避免了字符串的修改和从LocalDateTime
到LocalDate
的转换。此外,由于 Z
只能出现在字符串的最后,我的版本提供了更精确的验证。最后,作为品味问题,我喜欢 String.isEmpty
方法。
我有一个通用的 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);
我已经避免了字符串的修改和从LocalDateTime
到LocalDate
的转换。此外,由于 Z
只能出现在字符串的最后,我的版本提供了更精确的验证。最后,作为品味问题,我喜欢 String.isEmpty
方法。