日期时间类型参数
概述
前端传入的日期时间类型格式与后端要求的匹配则会导致报错,并且无法通过控制器中的断点检查传入参数,要查看传入参数需要定位到 spring 的源码 org.springframework.web.servlet.DispatcherServlet。或者可以抛出异常给前端,以便直观的发现错误,在 “统一异常处理” 中处理 HttpMessageNotReadableException 来给前端反馈信息。可参考文章: 接口中如何优雅的接收时间类型参数
最佳实践
在 visitor 后台项目的路径 src/main/java/com/cc/visitor/config 下制作两个自定义反序列化工具类 MultiFormatDeserializer4LocalDatetime 和 MultiFormatDeserializer4Date ,使用方法如下
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = MultiFormatDeserializer4Date.class)
private Date updateTime;
解决方法
概述
- 控制器中通过注解
@InitBinder
青铜
我们知道 SpringMVC 接收参数时自动将参数注入到我们的 JAVA 对象中是在 WebDataBinder 中实现的,SpringMVC 给我们提供了 @InitBinder,可以在接收参数之前对参数解析进行初始化设置,那我们可以在 Controller 中增加 @InitBinder,然后拿到 WebDataBinder 对象,自定义 LocalDateTime 和 Date 两种 CustomEditor 这样我们使用@PathVariable 和 @RequestParam 时就可以自动将 String 转成时间格式了。但是 @RequestBody 默认是使用Jackson 做 JSON 数据解析的,所以还是不能处理对象中的时间格式,我们可以在时间字段上增加 @JsonFormat 注解来指定时间格式,从而让 @RequestBody 也可以自动解析时间格式。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(DateUtil.parseLocalDateTime(text));
}
});
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(DateUtil.parse(text,NORM_DATETIME_PATTERN,NORM_DATE_PATTERN));
}
});
}
@Data
public class UserDTO {
private Long id;
private String userName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date now;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date day;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime time;
//private LocalDateTime timeStack;
}
该解析方案存在的问题:
@InitBinder作用域只是当前的Controller,如果我用100个Controller难道我要写100个@InitBinder@JsonFormat也是每个字段上都要增加个注解,而且只能支持一种时间格式,如果我们还要支持时间戳格式就没法做到了。
白银
针对青铜方案,可以采用 @ControllerAdvice 截断所有 controller 统一处理,就不用每个 controller 都使用 @InitBinder 了
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(DateUtil.parseLocalDateTime(text));
}
});
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(DateUtil.parse(text,NORM_DATETIME_PATTERN,NORM_DATE_PATTERN));
}
});
}
}
同时还要解决 SpringMvc 解析 JSON 使用的是 Jackson ,那么我们就可以让 SpringMVC 使用我们自定义的 Mapper 来解析 JSON, 我们在 @Configuration 增加ObjectMapper, 然后自定义 LocalDateTimeSerializer 和 LocalDateTimeDeserializer 的序列化和反序列化处理器,这样我们就不需要每个字段都添加上 @JsonFormat 了,Jaskson 在解析 JSON 数据时遇到参数接收类型是 LocalDateTime 类型时会直接使用我们的自定义处理器,这样就不会报字段转换错误了,也避免了一个一个写 @JsonFormat 。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
module.addSerializer(Date.class, new DateTimeSerializer());
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
module.addDeserializer(Date.class, new DateTimeDeserializer());
mapper.registerModule(module);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return JsonUtils.getMapper();
}
}
public class DateTimeDeserializer extends StdDeserializer<Date> {
public DateTimeDeserializer() {
this(null);
}
public DateTimeDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Date deserialize(JsonParser jp, DeserializationContext ctx)
throws IOException {
String value = jp.getValueAsString();
return DateUtil.parse(value,NORM_DATETIME_PATTERN,NORM_DATE_PATTERN);
}
}
public class DateTimeSerializer extends StdSerializer<Date> {
public DateTimeSerializer() {
this(null);
}
public DateTimeSerializer(Class<Date> t) {
super(t);
}
@Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(DateUtil.format(value, DatePattern.NORM_DATETIME_PATTERN));
}
}
public class LocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
public LocalDateTimeDeserializer() {
this(null);
}
public LocalDateTimeDeserializer(Class<?> vc) {
super(vc);
}
@Override
public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctx)
throws IOException {
String value = jp.getValueAsString();
if (StrUtil.isNumeric(value)) {
Date date = new Date(jp.getLongValue());
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("Asia/Shanghai"));
} else {
return DateUtil.parseLocalDateTime(value);
}
}
}
public class LocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
public LocalDateTimeSerializer() {
this(null);
}
public LocalDateTimeSerializer(Class<LocalDateTime> t) {
super(t);
}
@Override
public void serialize(LocalDateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(LocalDateTimeUtil.formatNormal(value));
}
}
该方案的问题: @ControllerAdvice 基于切面去做拦截,每个接口都需要经过拦截,性能和优雅性不是很好,能不能像 Jackson 一样优雅的处理呢?
王者
我们在 Configuration 中添加 Converter<String, LocalDateTime> stringLocalDateTimeConverter() 和 Converter<String, Date> stringDateTimeConverter(),自定义Converter 转换时间类型, 这样不管你是 JSON 数据传参还是 URL 传参数或是 Header 传参,也不管你接收的时间是类型使用 Date 还是 LocalDateTime,更不管你的时间格式是标准时间格式还是时间戳,统统自动解决了时间自动接收问题。
@Configuration
public class WebConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = JsonUtils.getMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
module.addSerializer(Date.class, new DateTimeSerializer());
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
module.addDeserializer(Date.class, new DateTimeDeserializer());
mapper.registerModule(module);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
@Bean
public Converter<String, LocalDateTime> stringLocalDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
if (StrUtil.isNumeric(source)) {
return LocalDateTimeUtil.of(Long.parseLong(source));
} else {
return DateUtil.parseLocalDateTime(source);
}
}
};
}
@Bean
public Converter<String, Date> stringDateTimeConverter() {
return new Converter<String, Date>() {
@Override
public Date convert(String source) {
if (StrUtil.isNumeric(source)) {
return new Date(Long.parseLong(source));
} else {
return DateUtil.parse(source);
}
}
};
}
}
