• 接口中如何优雅的接收时间类型参数


    首发:公众号《赵侠客

    前言

    在上文中我们总结了前后端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;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果:

    如果不做任务处理,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';
    
    
    • 1
    • 2
    • 3
    • 4

    青铜解决方案

    我们知道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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    青铜解析方案存在的问题:

    1. @InitBinder作用域只是当前的Controller,如果我用100个Controller难道我要写100个@InitBinder
    2. @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));
                }
            });
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    针对青铜方案存在的问题2,我们的分析是,既然SpringMvc解析JSON使用的是Jackson ,那么我们就可以让SpringMVC使用我们自定义的Mapper来解析JSON, 我们在@Configuration增加ObjectMapper
    然后自定义LocalDateTimeSerializerLocalDateTimeDeserializer的序列化的反序处理器,这样我们就不需要每个字段都添加上@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));
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    存在问题:

    1. @ControllerAdvice基于切面去做拦截,每个接口都需要经过拦截,性能和优雅性不是很好,能不能像Jackson一样优雅的处理呢?

    王者解决方案

    我们在Configuration中添加Converter stringLocalDateTimeConverter()Converter 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);
                    }
                }
            };
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    总结

    本文介绍了在SpringBoot项目开发中如何优雅的接收HTTP协议中的时间类型的参数。时间参数可以出现在URL Path、queryString、FormData、BodyJSON、HTTP header中,时间格式可以是标题格式,时间戳,接收时间参数可以是Date,LocalDateTime,非常优雅的全局处理了接口中接口时间类型字段问题。

  • 相关阅读:
    http和https请求总结
    Spark学习笔记(一):基于Kubernetes安装Spark
    计算机网络的相关知识点总结
    Webmin -- Disk and Network Filesystems
    创邻科技Galaxybase—激活数据要素的核心引擎
    计算机网络自学笔记008_Real(应用层)
    uniapp H5页面调用微信支付
    Android 13.0 framework层系统手势增加上滑手势home事件功能(相当于Home键)
    Pytorch(二) —— 激活函数、损失函数及其梯度
    基于libjpeg-turbo库的jpeg4py安装与使用记录
  • 原文地址:https://blog.csdn.net/whzhaochao/article/details/132939209