ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring Boot 别再用 Date 作为入参了,LocalDateTime、LocalDate 真香! [打印本页]

作者: 羊蹓狼    时间: 2023-9-2 09:37
标题: Spring Boot 别再用 Date 作为入参了,LocalDateTime、LocalDate 真香!
作者:TinyThing
链接:https://www.jianshu.com/p/b52db905f020
0x0 背景

项目中使用LocalDateTime系列作为dto中时间的类型,但是spring收到参数后总报错,为了全局配置时间类型转换,尝试了如下3中方法。
注:本文基于Springboot2.0测试,如果无法生效可能是spring版本较低导致的。PS:如果你的Controller中的LocalDate类型的参数啥注解(RequestParam、PathVariable等)都没加,也是会出错的,因为默认情况下,解析这种参数使用ModelAttributeMethodProcessor进行处理,而这个处理器要通过反射实例化一个对象出来,然后再对对象中的各个参数进行convert,但是LocalDate类没有构造函数,无法反射实例化因此会报错!!!
0x1 当LocalDateTime作为RequestParam或者PathVariable时

这种情况要和时间作为Json字符串时区别对待,因为前端json转后端pojo底层使用的是Json序列化Jackson工具(HttpMessgeConverter);而时间字符串作为普通请求参数传入时,转换用的是Converter,两者有区别哦。
在这种情况下,有如下几种方案:
推荐一个开源免费的 Spring Boot 实战项目:
https://github.com/javastacks/spring-boot-best-practice
1. 使用Converter
  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.core.convert.converter.Converter;
  4. import org.springframework.http.converter.HttpMessageConverter;
  5. import java.time.LocalDate;
  6. import java.time.LocalDateTime;
  7. import java.time.format.DateTimeFormatter;
  8. @Configuration
  9. public class DateConfig {
  10.     @Bean
  11.     public Converter<String, LocalDate> localDateConverter() {
  12.         return new Converter<>() {
  13.             @Override
  14.             public LocalDate convert(String source) {
  15.                 return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  16.             }
  17.         };
  18.     }
  19.     @Bean
  20.     public Converter<String, LocalDateTime> localDateTimeConverter() {
  21.         return new Converter<>() {
  22.             @Override
  23.             public LocalDateTime convert(String source) {
  24.                 return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  25.             }
  26.         };
  27.     }
  28. }
复制代码
以上两个bean会注入到spring mvc的参数解析器(好像叫做ParameterConversionService),当传入的字符串要转为LocalDateTime类时,spring会调用该Converter对这个入参进行转换。
2. 使用ControllerAdvice配合initBinder
  1. @ControllerAdvice
  2. public class GlobalExceptionHandler {
  3.     @InitBinder
  4.     protected void initBinder(WebDataBinder binder) {
  5.         binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
  6.             @Override
  7.             public void setAsText(String text) throws IllegalArgumentException {
  8.                 setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
  9.             }
  10.         });
  11.         binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
  12.             @Override
  13.             public void setAsText(String text) throws IllegalArgumentException {
  14.                 setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
  15.             }
  16.         });
  17.         binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
  18.             @Override
  19.             public void setAsText(String text) throws IllegalArgumentException {
  20.                 setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm:ss")));
  21.             }
  22.         });
  23.     }
  24. }
复制代码
从名字就可以看出来,这是在controller做环切(这里面还可以全局异常捕获),在参数进入handler之前进行转换;转换为我们相应的对象。
0x2 当LocalDateTime作为Json形式传入

这种情况下,如同上文描述,要利用Jackson的json序列化和反序列化来做:
  1. @Configuration
  2. public class JacksonConfig {
  3.     /** 默认日期时间格式 */
  4.     public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  5.     /** 默认日期格式 */
  6.     public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  7.     /** 默认时间格式 */
  8.     public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
  9.     @Bean
  10.     public ObjectMapper objectMapper(){
  11.         ObjectMapper objectMapper = new ObjectMapper();
  12. //            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  13. //            objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
  14.         JavaTimeModule javaTimeModule = new JavaTimeModule();
  15.         javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  16.         javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  17.         javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  18.         javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  19.         javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  20.         javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  21.         objectMapper.registerModule(javaTimeModule).registerModule(new ParameterNamesModule());
  22.         return objectMapper;
  23.     }
  24. }
复制代码
0x3 来个完整的配置吧

Spring Boot 基础就不介绍了,推荐看这个实战项目:
https://github.com/javastacks/spring-boot-best-practice
  1. package com.fly.hi.common.config;
  2. import com.fasterxml.jackson.core.JsonGenerator;
  3. import com.fasterxml.jackson.core.JsonParser;
  4. import com.fasterxml.jackson.core.JsonProcessingException;
  5. import com.fasterxml.jackson.databind.*;
  6. import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  7. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
  8. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
  9. import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
  10. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
  11. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
  12. import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
  13. import org.springframework.context.annotation.Bean;
  14. import org.springframework.context.annotation.Configuration;
  15. import org.springframework.core.convert.converter.Converter;
  16. import java.io.IOException;
  17. import java.text.ParseException;
  18. import java.text.SimpleDateFormat;
  19. import java.time.LocalDate;
  20. import java.time.LocalDateTime;
  21. import java.time.LocalTime;
  22. import java.time.format.DateTimeFormatter;
  23. import java.util.Date;
  24. @Configuration
  25. public class DateConfig {
  26.     /** 默认日期时间格式 */
  27.     public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  28.     /** 默认日期格式 */
  29.     public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  30.     /** 默认时间格式 */
  31.     public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
  32.     /**
  33.      * LocalDate转换器,用于转换RequestParam和PathVariable参数
  34.      */
  35.     @Bean
  36.     public Converter<String, LocalDate> localDateConverter() {
  37.         return new Converter<>() {
  38.             @Override
  39.             public LocalDate convert(String source) {
  40.                 return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
  41.             }
  42.         };
  43.     }
  44.     /**
  45.      * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
  46.      */
  47.     @Bean
  48.     public Converter<String, LocalDateTime> localDateTimeConverter() {
  49.         return new Converter<>() {
  50.             @Override
  51.             public LocalDateTime convert(String source) {
  52.                 return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));
  53.             }
  54.         };
  55.     }
  56.     /**
  57.      * LocalTime转换器,用于转换RequestParam和PathVariable参数
  58.      */
  59.     @Bean
  60.     public Converter<String, LocalTime> localTimeConverter() {
  61.         return new Converter<>() {
  62.             @Override
  63.             public LocalTime convert(String source) {
  64.                 return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
  65.             }
  66.         };
  67.     }
  68.     /**
  69.      * Date转换器,用于转换RequestParam和PathVariable参数
  70.      */
  71.     @Bean
  72.     public Converter<String, Date> dateConverter() {
  73.         return new Converter<>() {
  74.             @Override
  75.             public Date convert(String source) {
  76.                 SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
  77.                 try {
  78.                     return format.parse(source);
  79.                 } catch (ParseException e) {
  80.                     throw new RuntimeException(e);
  81.                 }
  82.             }
  83.         };
  84.     }
  85.     /**
  86.      * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
  87.      */
  88.     @Bean
  89.     public ObjectMapper objectMapper(){
  90.         ObjectMapper objectMapper = new ObjectMapper();
  91.         objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  92.         objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
  93.         //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
  94.         JavaTimeModule javaTimeModule = new JavaTimeModule();
  95.         javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  96.         javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  97.         javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  98.         javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  99.         javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  100.         javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  101.         //Date序列化和反序列化
  102.         javaTimeModule.addSerializer(Date.class, new JsonSerializer<>() {
  103.             @Override
  104.             public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  105.                 SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
  106.                 String formattedDate = formatter.format(date);
  107.                 jsonGenerator.writeString(formattedDate);
  108.             }
  109.         });
  110.         javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<>() {
  111.             @Override
  112.             public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
  113.                 SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
  114.                 String date = jsonParser.getText();
  115.                 try {
  116.                     return format.parse(date);
  117.                 } catch (ParseException e) {
  118.                     throw new RuntimeException(e);
  119.                 }
  120.             }
  121.         });
  122.         objectMapper.registerModule(javaTimeModule);
  123.         return objectMapper;
  124.     }
  125. }
复制代码
0x4 深入研究SpringMVC数据绑定过程

接下来进入debug模式,看看mvc是如何将我们request中的参数绑定到我们controller层方法入参的:
写一个简单controller,下个断点看看方法调用栈:
  1. @GetMapping("/getDate")
  2. public LocalDateTime getDate(@RequestParam LocalDate date,
  3.                              @RequestParam LocalDateTime dateTime,
  4.                              @RequestParam Date originalDate) {
  5.     System.out.println(date);
  6.     System.out.println(dateTime);
  7.     System.out.println(originalDate);
  8.     return LocalDateTime.now();
  9. }
复制代码
断住以后,我们看下方法调用栈中一些关键方法:
  1. //进入DispatcherServlet
  2. doService:942, DispatcherServlet
  3. //处理请求
  4. doDispatch:1038, DispatcherServlet
  5. //生成调用链(前处理、实际调用方法、后处理)
  6. handle:87, AbstractHandlerMethodAdapter
  7. //反射获取到实际调用方法,准备开始调用
  8. invokeHandlerMethod:895, RequestMappingHandlerAdapter
  9. invokeAndHandle:102, ServletInvocableHandlerMethod
  10. //这里是关键,参数从这里开始获取到
  11. invokeForRequest:142, InvocableHandlerMethod
  12. doInvoke:215, InvocableHandlerMethod
  13. //这个是Java reflect调用,因此一定是在这之前获取到的参数
  14. invoke:566, Method
复制代码
根据上述分析,发现invokeForRequest:142, InvocableHandlerMethod这里的代码是用来拿到实际参数的:
  1. @Nullable
  2. public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  3.         Object... providedArgs) throws Exception {
  4.     //这个方法是获取参数的,在这里下个断
  5.     Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
  6.     if (logger.isTraceEnabled()) {
  7.         logger.trace("Arguments: " + Arrays.toString(args));
  8.     }
  9.     //这里开始调用方法
  10.     return doInvoke(args);
  11. }
复制代码
进入这个方法看看是什么操作:
  1. protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  2.         Object... providedArgs) throws Exception {
  3.     //获取方法参数数组,包含了入参信息,比如类型、泛型等等
  4.     MethodParameter[] parameters = getMethodParameters();
  5.     //这个用来存放一会从request parameter转换的参数
  6.     Object[] args = new Object[parameters.length];
  7.     for (int i = 0; i < parameters.length; i++) {
  8.         MethodParameter parameter = parameters[i];
  9.         parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
  10.         //这里看起来没啥卵用(providedArgs为空)
  11.         args[i] = resolveProvidedArgument(parameter, providedArgs);
  12.         //这里开始获取到方法实际调用的参数,步进
  13.         if (this.argumentResolvers.supportsParameter(parameter)) {
  14.             //从名字就看出来:参数解析器解析参数
  15.             args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
  16.             continue;
  17.         }
  18.     }
  19.     return args;
  20. }
复制代码
进入resolveArgument看看:
  1. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  2.         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  3.     //根据方法入参,获取对应的解析器
  4.     HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  5.      //开始解析参数(把请求中的parameter转为方法的入参)
  6.     return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  7. }
复制代码
这里根据参数获取相应的参数解析器,看看内部如何获取的:
  1. //遍历,调用supportParameter方法,跟进看看
  2. for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
  3.     if (methodArgumentResolver.supportsParameter(parameter)) {
  4.         result = methodArgumentResolver;
  5.         this.argumentResolverCache.put(parameter, result);
  6.         break;
  7.     }
  8. }
复制代码
这里,遍历参数解析器,查找有没有适合的解析器!那么,有哪些参数解析器呢(我测试的时候有26个)???我列出几个重要的看看,是不是很眼熟!!!
  1. {RequestParamMethodArgumentResolver@7686}
  2. {PathVariableMethodArgumentResolver@8359}
  3. {RequestResponseBodyMethodProcessor@8366}
  4. {RequestPartMethodArgumentResolver@8367}
复制代码
我们进入最常用的一个解析器看看他的supportsParameter方法,发现就是通过参数注解来获取相应的解析器的。
  1. public boolean supportsParameter(MethodParameter parameter) {
  2.     //如果参数拥有注解@RequestParam,则走这个分支(知道为什么上文要对RequestParam和Json两种数据区别对待了把)
  3.     if (parameter.hasParameterAnnotation(RequestParam.class)) {
  4.         //这个似乎是对Optional类型的参数进行处理的
  5.         if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
  6.             RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
  7.             return (requestParam != null && StringUtils.hasText(requestParam.name()));
  8.         }
  9.         else {
  10.             return true;
  11.         }
  12.     }
  13.     //......
  14. }
复制代码
也就是说,对于@RequestParam和@RequestBody以及@PathVariable注解的参数,SpringMVC会使用不通的参数解析器进行数据绑定!
那么,这三种解析器分别使用什么Converter解析参数呢?我们分别进入三种解析器看一看:
首先看下RequestParamMethodArgumentResolver发现内部使用WebDataBinder进行数据绑定,底层使用的是ConversionService (也就是我们的Converter注入的地方)
  1. WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
  2. //通过DataBinder进行数据绑定的
  3. arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
复制代码
  1. //跟进convertIfNecessary()
  2. public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  3.         @Nullable MethodParameter methodParam) throws TypeMismatchException {
  4.     return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
  5. }
复制代码
  1. //继续跟进,看到了把
  2. ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
  3. if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
  4.     TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
  5.     if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
  6.         try {
  7.             return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
  8.         }
  9.         catch (ConversionFailedException ex) {
  10.             // fallback to default conversion logic below
  11.             conversionAttemptEx = ex;
  12.         }
  13.     }
  14. }
复制代码
然后看下RequestResponseBodyMethodProcessor发现使用的转换器是HttpMessageConverter类型的:
  1. //resolveArgument方法内部调用下面进行参数解析
  2. Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
  3. //step into readWithMessageConverters(),我们看到这里的Converter是HttpMessageConverter
  4. for (HttpMessageConverter<?> converter : this.messageConverters) {
  5.     Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
  6.     GenericHttpMessageConverter<?> genericConverter =
  7.             (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
  8.     if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
  9.             (targetClass != null && converter.canRead(targetClass, contentType))) {
  10.         if (message.hasBody()) {
  11.             HttpInputMessage msgToUse =
  12.                     getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
  13.             body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
  14.                     ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
  15.             body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
  16.         }
  17.         else {
  18.             body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
  19.         }
  20.         break;
  21.     }
  22. }
复制代码
最后看下PathVariableMethodArgumentResolver发现 和RequestParam走的执行路径一致(二者都是继承自AbstractNamedValueMethodArgumentResolver解析器),因此代码就不贴了。
0xFF总结

如果要转换request传来的参数到我们指定的类型,根据入参注解要进行区分:
近期热文推荐:
1.1,000+ 道 Java面试题及答案整理(2022最新版)
2.劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
5.《Java开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4