跳至主要內容

日期时间类型参数

chanchaw大约 4 分钟languagejava

概述

前端传入的日期时间类型格式与后端要求的匹配则会导致报错,并且无法通过控制器中的断点检查传入参数,要查看传入参数需要定位到 spring 的源码 org.springframework.web.servlet.DispatcherServlet。或者可以抛出异常给前端,以便直观的发现错误,在 “统一异常处理” 中处理 HttpMessageNotReadableException 来给前端反馈信息。可参考文章: 接口中如何优雅的接收时间类型参数open in new window

最佳实践

visitor 后台项目的路径 src/main/java/com/cc/visitor/config 下制作两个自定义反序列化工具类 MultiFormatDeserializer4LocalDatetimeMultiFormatDeserializer4Date ,使用方法如下

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = MultiFormatDeserializer4Date.class)
private Date updateTime;

解决方法

概述

  1. 控制器中通过注解 @InitBinder

青铜

我们知道 SpringMVC 接收参数时自动将参数注入到我们的 JAVA 对象中是在 WebDataBinder 中实现的,SpringMVC 给我们提供了 @InitBinder,可以在接收参数之前对参数解析进行初始化设置,那我们可以在 Controller 中增加 @InitBinder,然后拿到 WebDataBinder 对象,自定义 LocalDateTimeDate 两种 CustomEditor 这样我们使用@PathVariable@RequestParam 时就可以自动将 String 转成时间格式了。但是 @RequestBody 默认是使用JacksonJSON 数据解析的,所以还是不能处理对象中的时间格式,我们可以在时间字段上增加 @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. @InitBinder 作用域只是当前的 Controller,如果我用100个 Controller 难道我要写100个 @InitBinder
  2. @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, 然后自定义 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));
    }
}

该方案的问题: @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);
                }
            }
        };
    }
}