关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
服务端接收参数的形式有多种,可以通过Form表单的形式接收,也可以使用@RequestBody来接收Json参数。但是接收参数过程中,唯独日期类型的参数总是出现解析异常。如下图:

近日,遇到同事求助说通过@RequestBody接受日期参数异常,如上图。项目框架是我搭建的,当时已经处理了关于日期参数传递的解析问题,还是出现问题有点不可思议。
我们一起来看看,怎么解决!
02 全局日期处理器
搭建框架初期,专门通过@InitBinder和@ControllerAdvice处理了日期,作为全局的日期处理器。
            
            
              java
              
              
            
          
          @Slf4j
@ControllerAdvice
public class GlobalWebBinderHandler {
    /**
     *  处理web的data数据,这里主要处理form表单的日期
     */
    @InitBinder
    public void globalWebDataBinder(WebDataBinder binder){
        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                Date date = null;
                try {
                    if (StringUtils.isNotBlank(text)) {
                        if (StringUtils.isNumeric(text)) {
                            date = Date.from(Instant.ofEpochMilli(Long.parseLong(text)));
                        }else {
                            date = DateUtil.parseDate(text);
                        }
                    }
                } catch (ParseException e) {
                    log.warn("web 日期参数解析异常,异常格式:{}, 支持的日期格式:{}",  
                             text, JSON.toJSONString(DateUtil.GENERIC_DATE_PATTERNS));
                }
                setValue(date);
            }
        });
    }
}
        @ControllerAdvice:用来拦截所有的@Controller控制层@InitBinder:用来处理Form表单传递的参数WebDataBinder:用来映射String参数为T,这里只处理了Date
DateUtil.parseDate(text)是通过DateUtil.GENERIC_DATE_PATTERN数组,依次解析日期,直到日期被正确解析,都则接收的值为NULL。
通过@InitBinder注解的注释可以看到,支持RequestMapping的所有参数,尤其Form表单参数。

而从遇到的问题看,这里的配置并没有走到。而是Jackson反序列化时遇到了异常。
03 @RequestBody
同时遇到的异常就是使用@RequestBody来接收JSON数据而导致的。这是怎么产生的呢?@RequestBody 是一个 Spring MVC 注解,它的主要作用是将 HTTP 请求体(Body)中的数据,按照特定的格式(如 JSON、XML)绑定到控制器方法的参数上。它通常用于处理 POST、PUT、PATCH 等非 GET 请求,这些请求的数据通常放在请求体中。
3.1 完整的解析流程
整个解析过程可以看作一条清晰的流水线,涉及了从 HTTP 请求到 Java 对象转化的每一步。下图清晰地展示了这一核心流程:

详细说明:
接收请求与分发
- ① HTTP 请求到达 :客户端发送一个 HTTP 请求(例如,Content-Type: 
application/json)。 - ② 
DispatcherServlet拦截 :作为前端控制器,DispatcherServlet会拦截所有请求。 - ③ 查找处理器 :
DispatcherServlet根据请求 URL,通过HandlerMapping找到对应的控制器(Controller)和处理方法(HandlerMethod)。 
准备调用处理方法
- ④ 
HandlerAdapter接手 :DispatcherServlet将请求交给RequestMappingHandlerAdapter来处理。 - ⑤ 解析方法参数 :
HandlerAdapter开始准备调用目标方法。它需要解析方法的每一个参数,并为每个参数赋值。这时,它会使用一系列HandlerMethodArgumentResolver(参数解析器)。 
识别 @RequestBody 注解
- ⑥ 找到合适的解析器 :
HandlerAdapter遍历所有参数解析器,当它遇到一个带有@RequestBody注解的参数时,会找到一个特定的解析器 ------RequestResponseBodyMethodProcessor。 - ⑦ 
RequestResponseBodyMethodProcessor工作 :这个解析器专门负责处理@RequestBody和@ResponseBody。 
读取请求体与内容协商
- ⑧ 获取输入流 :
RequestResponseBodyMethodProcessor从HttpServletRequest中获取请求体的输入流。 - ⑨ 内容协商 :它检查请求的 
Content-Type头(例如application/json),以确定客户端发送的数据格式。 - ⑩ 选择消息转换器 :根据 
Content-Type,它会从配置好的HttpMessageConverter列表中选择一个合适的转换器。对于application/json,默认使用的是MappingJackson2HttpMessageConverter(如果 Jackson 库在类路径上)。 
反序列化与类型转换
- ⑪ 反序列化(核心步骤) :
MappingJackson2HttpMessageConverter从输入流中读取原始的JSON字符串,使用 Jackson 的ObjectMapper来执行反序列化。 
数据验证
- ⑫ 执行校验 :如果参数上同时还使用了 
@Valid或@Validated注解,RequestResponseBodyMethodProcessor会在此刻触发JSR-303 Bean验证。 
方法调用与返回
- ⑬ 参数绑定完成:此时,一个完整的、填充好数据的 Java 对象已经准备就绪
 - ⑭ 调用控制器方法 :
HandlerAdapter用这个对象作为参数,调用你的控制器方法。 - ⑮ 处理响应 :方法执行完毕后,返回结果,流程进入 
@ResponseBody的序列化流程。 
3.2 关键代码追踪

通过跟踪代码,大致的流程如上图。

根据断点调试,我们会发现默认的MessageConverters有8个,第6个和第7个是专门解析application/json类型的。由于源码是按照顺序遍历,所以必然会取到第6个。

我们可以看到内存地址是@6766,验证了我们的猜想。
继续跟踪代码,就发现了我们之前的报错:

因为这里的反序列化框架使用的是Jackson,而Jackson默认的日期格式是:yyyy-MM-dd'T'HH:mm:ss.SSSX
04 问题解决
问题原因知道了,解决也就简单了。
4.1 局部解决
我们需要使用Jackson的一个注解:@JsonForma
            
            
              java
              
              
            
          
          @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+08:00")
        只需要在日期类型的字段上添加上面的注解,即可解决。一定要增加时区的修正,否则日期就会出现时差。

这种解决方案有其弊端。因为很多实体使用的是逆向工程,随时可能会被替换掉。还有就是这种作为需要为每一个日期字段增加注解。不能全局解决。
4.2 全局配置
Jackson框架有全局的配置,我们只需要修改全局配置即可解决。
            
            
              properties
              
              
            
          
          spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
        4.3 配置其他消息转化器
阿里开源的fajson或者fasjon2,本身默认就是yyyy-MM-dd HH:mm:ss日期格式。我们直接使用阿里的消息转化器器即可。
依赖引入
fasjon2作为第二代json处理工具,和第一代的引入方式有一些区别。这里需要引入fastjson2的扩展包,其中Spring的版本需要根据项目的版本引入。小编这里使用的是spring6。
            
            
              xml
              
              
            
          
          <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension-spring6</artifactId>
    <version>2.0.45</version>
</dependency>
        注册
            
            
              java
              
              
            
          
          @Configuration
public class BeanConfig implements WebMvcConfigurer {
     @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        // 这里可以按需指定日期格式
//        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        converter.setFastJsonConfig(fastJsonConfig);
        converters.add(0, converter);
    }
}
        fastJsonConfig 作为消息转化器的配置项,在项目中按需配置即可。从之前的源码来看,消息转化器是按照顺序解析的,我们需要将消自定的FastJsonHttpMessageConverter加载在前面。或者直接用set的方式替换默认的消息转化器。
05 小结
@RequestBody 的解析是一个由 DispatcherServlet -> HandlerAdapter -> RequestResponseBodyMethodProcessor -> HttpMessageConverter 协同完成的复杂过程。理解这个流程对于深入掌握 Spring MVC、高效处理 RESTful API 以及精准定位和解决相关问题至关重要。它完美地体现了 Spring 框架通过组件分工和策略接口提供的强大扩展能力和灵活性。
理解其原理,解决方案也就明朗了。