「String到Date转换失败」:深挖@RequestBody的日期坑

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

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)绑定到控制器方法的参数上。它通常用于处理 POSTPUTPATCH 等非 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

读取请求体与内容协商

  • 获取输入流RequestResponseBodyMethodProcessorHttpServletRequest 中获取请求体的输入流。
  • 内容协商 :它检查请求的 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 框架通过组件分工和策略接口提供的强大扩展能力和灵活性。

理解其原理,解决方案也就明朗了。

相关推荐
CryptoRzz3 小时前
python对接印度尼西亚股票数据接口文档
后端
渣哥4 小时前
Lazy能否有效解决循环依赖?答案比你想的复杂
javascript·后端·面试
qq_12498707534 小时前
基于Spring Boot的网上招聘服务系统(源码+论文+部署+安装)
java·spring boot·后端·spring·计算机外设
高山上有一只小老虎4 小时前
杨辉三角的变形
java·算法
代码小菜鸡6664 小时前
java 常用的一些数据结构
java·数据结构·python
止水编程 water_proof4 小时前
Java--网络编程(二)
java·开发语言·网络
少许极端4 小时前
算法奇妙屋(六)-哈希表
java·数据结构·算法·哈希算法·散列表·排序
Da Da 泓4 小时前
shellSort
java·数据结构·学习·算法·排序算法
武子康4 小时前
Java-148 深入浅出 MongoDB 聚合操作:$match、$group、$project、$sort 全面解析 Pipeline 实例详解与性能优化
java·数据库·sql·mongodb·性能优化·系统架构·nosql