驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
SpringBoot中时间戳和LocalDate相关的接收和转换
/  

SpringBoot中时间戳和LocalDate相关的接收和转换

背景

  1. 前端和后端在时间格式的传递上都走的是时间戳(方便前端自由定制)

  2. 时间格式大多用的是LocalDate,并不是传统的Date

简单来说就是时间戳在各类形式下转日期类。

当有如下类似代码的时候,后端该做哪些配置来满足上述需求了?

    @PostMapping("/date-test/post/{pathDate}")
    public LocalDateTime getDatePost(@RequestParam LocalDate date,
                                     @RequestParam LocalDateTime dateTime,
                                     @RequestParam Date originalDate,
                                     @PathVariable("pathDate") LocalDateTime datePath,
                                     LocalDate localDate,
                                     @RequestBody Person person,
                                     Person person2) {
        System.out.println(date);
        System.out.println(dateTime);
        System.out.println(originalDate);
        System.out.println(localDate);
        System.out.println(datePath);
        System.out.println(person);
        System.out.println(person2);

        return LocalDateTime.now();
    }
}

上述案例使用了post来接收参数,如果你是get请求,除了@RequestBody不能使用外,其他不受影响

请求的URL如下所示:

localhost:8099/date-test/post/1568796069368?date=1568796069368&localDate=1568796069368&dateTime=1568796069368&originalDate=1568796069368&name=codingOX&birth=1568796069368

其中json部分的配置如下

nHionK.png

正文

SpringMVC处理参数,其核心接口是org.springframework.web.method.support.HandlerMethodArgumentResolver

相关的实现挺多的,如下所示,重点部分我都通过红色框框进行了标记。

nHiwpn.png

我们通过接口来分析下SpringMVC是如何处理参数的

public interface HandlerMethodArgumentResolver {

	/**
	 * 判定当前处理器适合处理那种类型的数据
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * 对符合预期的数据,进行具体的逻辑处理
	 */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

更详细的代码你可以查看上图给出的实现,整体而言就是根据根据supportsParameter的结果Boolean来判定当前类是否可以解析这个参数,如果可以继续调用resolveArgument对参数进行解析。

后面我们需要利用该特点来自定义处理类,下面将通过如下几种情况进行处理。

  • 通过@RequestParam@PathVariable修饰LocalDate
  • 通过@RequestBody修饰LocalDate
  • 不对LocaDate添加任何修饰
  • 不对包含LocalDate的对象添加任何修饰

@RequestParam和@PathVariable

这种情况对应的是上面例子形参中的:

    public LocalDateTime getDatePost(@RequestParam LocalDate date,
                                     ....
                                     @PathVariable LocalDateTime datePath,
                                     ....) {...}

Spring的源码部分,通过文字分析和解读起来太累,这里直接给出结论,下同,有兴趣的可以结合结论,反过来阅读。。。

这2个类对应的处理器分别是:

  • org.springframework.web.method.annotation.RequestParamMethodArgumentResolver
  • org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver

如果有兴趣,可以跟下源码,你可以发现,处理这2类注解的解析主要用到的是:WebDataBinder,如果要扩展这类参数的处理方式,可以使用org.springframework.core.convert.converter.Converter或者org.springframework.format.Formatter

这2者区别在于:

  • Converter可以定义输入和输出
  • Formatter默认输入是字符串,输出为自定义

你可以根据自身需求进行定制,这里我通过Convert转换LocalDate为例,进行转换:

/**
 * 接收 形参加有注解  @RequestParam和@PathVariable时候 字符串转Date相关类
 *
 * @author Liu Chunfu
 * @date 2019-09-18 3:57 下午
 **/
public class CustomDateConverter {
  
    public static class LocalDateConvert implements Converter<String, LocalDate> {
        @Override
        public LocalDate convert(String timestamp) {
            return LocalDateUtil.timestampToLocalDateTime(timestamp).toLocalDate();
        }
    }
  
   ...
}

定义后,还需要使用,这个使用也很简单,将其注入Spring的容器就可以了。

  • 如果是Spring MVC,通过XML定义为类
  • 如果是Spring Boot,在@Configuration下通过@Bean声明就可以了。

还是举个例子吧:

@Configuration
public class CustomDateConfig{
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        //此处不能替换为lambda表达式
        return new CustomDateConverter.LocalDateConvert();
    }
}

通过@RequestBody修饰

通过@RequestBody很明显的就是要通过JSON序列化进行处理,Spring默认的是Jackson进行处理,所以我们需要对默认的JSON处理器进行日期类型的添加(假如默认不带对应序列化器的情况下)

此时需要用的是JsonDeserializer这个类,对应的实现如下所示:

public class CustomDateDeserializer {

    /**
     * 反序列化LocalDateTime
     */
    public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {

        @Override
        public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            Long timestamp = Long.valueOf(p.getText());
            Instant instant = Instant.ofEpochMilli(timestamp);
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("+8"));
            LocalDate localDate = localDateTime.toLocalDate();
            return localDate;
        }
    }
  
  ....
}

有了这个了类,还需要添加到Spring中,Spring利用的是ObjectMapper,我们自定义一个ObjectMapper替换原本的就可以了。

@Configuration
public class CustomDateConfig  {

	  // 省略部分代码

    /**
     * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
     */
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        //不显示为null的字段
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 忽略不能转移的字符
        objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
        // 过滤对象的null属性.
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //忽略transient
        objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);

        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

        //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        // LocalDateTime
        javaTimeModule.addSerializer(LocalDateTime.class, new CustomDateSerializer.LocalDateTimeSerializer());
        javaTimeModule.addDeserializer(LocalDateTime.class,new CustomDateDeserializer.LocalDateTimeDeserializer());
        // LocalDate
        javaTimeModule.addSerializer(LocalDate.class, new CustomDateSerializer.LocalDateSerializer());
        javaTimeModule.addDeserializer(LocalDate.class, new CustomDateDeserializer.LocalDateDeserializer());
        //Date序列化和反序列化
        javaTimeModule.addSerializer(Date.class,new CustomDateSerializer.DateSerializer());
        javaTimeModule.addDeserializer(Date.class,new CustomDateDeserializer.DateDeserializer());

        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }
  // 省略部分代码
}

外部参数什么注解也不加

这种情况对应的是形参中的:

public LocalDateTime getDatePost(...,LocalDate localDate,....) { .... }

这种情况下,通过Spring默认的ModelAttributeMethodProcessor进行处理,其处理逻辑为:通过反射创建一个对象,然会在反射对每个参数通过Convert进行转换赋值。

默认情况下,此种情况是会报错的,因为LocalDate没有构造函数。所以我们需要在默认调用之前,自定义处理这此类形参,此时利用的就是HandlerMethodArgumentResolver这个接口。

HandlerMethodArgumentResolver会在ModelAttributeMethodProcessor之前调用,该接口的声明非常简单。

todo

我们可以对此类参数进行自定义。

public class CustomDateArgumentResolverHandler {

    public static class LocalDateArgumentResolverHandler implements HandlerMethodArgumentResolver {

        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.getParameterType().equals(LocalDate.class);
        }

        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

            String timestamp = webRequest.getParameter(parameter.getParameterName());
            LocalDateTime localDateTime = LocalDateUtil.timestampToLocalDateTime(timestamp);
            return localDateTime.toLocalDate();
        }
    }
  
  ......
}

在使用该类的时候,需要在配置类@Configuration中通过WebMvcConfigurer这个接口下的方法addArgumentResolvers来添加进去:

public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) 

大体的代码如下:

@Configuration
public class CustomDateConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CustomDateArgumentResolverHandler.LocalDateArgumentResolverHandler());
        // .....
    }

}

内部参数什么注解也不加

对应的情况是:

public LocalDateTime getDatePost(Person person2) 

此时的处理情况符合上述提到的ModelAttributeMethodProcessorConvert处理。

所以如果我们按照上面提到的方法进行了处理,那么不用担心时间类型无法解析的问题。

结语

关于时间LocalDate这个类型的处理都完成了,上面的代码还是比较凌乱的,改天我单独做一个Demo,放在GitHub上,不过我还是希望你手动自己试试!

积土成山,风雨兴焉。积水成渊,蛟龙生焉。