详解springboot接口如何优雅的接收时间类型参数
作者:赵侠客
前言
在上文中我们总结了前后端Http接口传参的常用方法,本文主要针对参数中的时间字段如何处理做个总结,由于时间的格式有很多种,比较常用的有时间戳格式、UTC时间格式、标准时间格式等,而且时间参数出现的位置可能在URL上,可能在Body中,也可能在Header中,所以本文提供一套优雅的处理时间格式字段的解决方案。
时间格式不做任务处理会怎样?
我们创建一个简单的接口,想通过@PathVariable
接收Date类型的时间参数,和通过@RequestParam
接收LocalDateTime类型的时间参数,并且想通过@RequestBody
来接收JSON中的时间参数:
@GetMapping("/time/{today}") public UserDTO time(@PathVariable Date today, @RequestParam LocalDateTime time,@RequestBody UserDTO userDTO) { return userDTO; } @Data public class UserDTO { private Long id; private String userName; private Date now; private Date day; private LocalDateTime time; private LocalDateTime timeStack; }
HTTP测试请求报文:
GET http://localhost:80/time/2023-09-10?time=2023-09-15 11:11:11 Accept: application/json Content-Type: application/json { "id":1, "now":"2023-09-15 13:50:10", "day":"2023-09-15", "time": "2023-09-15 13:50:10", "timeStack": 1694757010407 }
结果:
如果不做任务处理,SpringBoot是不能自动帮我们把接口中的时间参数转成我们想要的时间格式的,默认是用String接收的,如果直接用LocalDateTime或者Date来接收会报一个类型转换错误。这个也是比较好理解的,因为时间格式太多了,在不知道具体时间格式的情况下,框架也无法解析时间,只能用String接收了,最后将String转成时间类型肯定就报错了。当然我们可以使用String接收,再手动转成对应的时间格式,这个方法太原始了,接下来我们看看不同级别是如何处理时间字段的。
Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime';
Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date';
青铜解决方案
我们知道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
也是每个字段上都要增加个注解,而且只能支持一种时间格式,如果我们还要支持时间戳格式就没法做到了。
白银解决方案
针对青铜解析方案存在的问题1,我们的解决方案是使用@ControllerAdvice
,这样就不用在每个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)); } }); } }
针对青铜方案存在的问题2,我们的分析是,既然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 = 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(); } @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); } } }; } }
总结
本文介绍了在SpringBoot项目开发中如何优雅的接收HTTP协议中的时间类型的参数。时间参数可以出现在URL Path、queryString、FormData、BodyJSON、HTTP header中,时间格式可以是标题格式,时间戳,接收时间参数可以是Date,LocalDateTime,非常优雅的全局处理了接口中接口时间类型字段问题。
到此这篇关于详解springboot接口如何优雅的接收时间类型参数的文章就介绍到这了,更多相关springboot接收参数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!