SpringMVC Date类型参数解析

前端向后端传递Date数据,后端有三种实现方式,@RequestBody注解、@RequestParam注解和不加注解。日期数据的形式也有两种,时间戳和日期字符串。

情况一:后端用@RequestBody注解标记。在这种情况下,Date又可以分两种数据存在方式:一种是单独作为接口的参数来传递,另外一种是作为参数对象属性来传递。

1.Date类型数据单独作为接口的参数传递。在默认情况(不实现任何日期转换接口或方法)下,前端向后端传递日期数据时,前端日期数据需要转换成时间戳,代码如下所示。如果前端向后端传的数据是日期字符串,会抛出相应的异常(Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('-' (code 45)): Expected space separating root-level values,我这里的日期格式是:“yyyy-MM-dd HH:mm:ss”)。如果在不实现任何日期转换接口或方法情况下,想要实现前端向后端传递日期字符串的功能,后端只能将日期类型参数封装到参数对象中,作为Date作为参数对象的属性来传递。

   示例代码1 (Date单独作为接口的参数)

    @Test

    public void testRequestBodyDateTest() {

        String params = "1608537480434";

        String jsonStr = StringUtils.replaceSingleQuoteWithDouble(params);

                    this.postBody("/testRequestBodyDate/", jsonStr);

    }

    @PutMapping("/testRequestBodyDate/")

    public CommonResult  testRequestBodyDate(@RequestBody Date test){

        logger.info("testRequestBodyDate");

        return CommonResult.success(test);

    }

 

2.Date类型数据作为参数对象属性传递。在默认情况(不实现任何日期转换接口或方法)下,前端的日期数据只能是日期字符串,代码如下所示。如果是时间戳,org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "1608537480434": not a valid representation (error: Failed to parse Date value '1608537480434': Unparseable date: "1608537480434");。

示例代码2 (Date作为参数对象属性)

    @Test

    public void testBodyTest() {

        String params = "{intTest':1,'longTest':0,'dateTest':'2020-12-21 15:58:00'}";

        String jsonStr = StringUtils.replaceSingleQuoteWithDouble(params);

                    this.postBody("/testRequestBody/", jsonStr);

    }


    @PostMapping("/testRequestBody/")

    public CommonResult  testBody(@RequestBody TestObject testObject){

        logger.info("testBody");

        return CommonResult.success(testObject);

    }

情况二:后端用@RequestParam注解或者没有注解的方式接收前端日期字符串。在没有实现日期转换相关接口情况下,代码如下示例代码3所示,会出现的异常(org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2020-12-21 17:14:44'; nested exception is java.lang.IllegalArgumentException)。此时Date数据不管是单独作为接口的参数,还是作为参数对象属性,后端都必须实现日期转换接口或方法,在转换接口或方法中实现日期转换功能,此时,前端传到后端的日期数据可以是时间戳,也可以是日期字符串,具体逻辑由开发者自己决定,这里有两种实现方法。方法一是为controller编写一个基类,在基类中实现一个方法,用@InitBinder注解,并绑定DataBinder(WebDataBinder ),如示例代码4;方法二是实现Converter接口,如下示例代码5。

示例代码3

     @Test

    public void testDateTest() {

        String params = "{'test':'2020-12-21 15:58:00'}";

        String jsonStr = StringUtils.replaceSingleQuoteWithDouble(params);

                    this.post("/testDate/", jsonStr);

    }

    @PutMapping("/testDate/")

    public CommonResult  testDate(@RequestParam Date test){

        logger.info("testDate");

        return CommonResult.success(test);

    }

示例代码4(方法一)

    /**

     * 将前台传递过来的日期格式的字符串,自动转化为Date类型

     */

    @InitBinder

    public void initBinder(WebDataBinder binder)

    {

        // Date 类型转换

        binder.registerCustomEditor(Date.class, new PropertyEditorSupport()

        {

            @Override

            public void setAsText(String text)

            {

                setValue(DateUtils.parseDate(text, "yyyy-MM-dd HH:mm:ss"));

            }

        });

    }

示例代码5(方法二)

@Configuration

public class AnerTesterConfig implements Converter<String, Date> {



    private static Logger logger = LoggerFactory.getLogger(AnerTesterConfig.class);



    @Override

    public Date convert(String source) {

        String value = source.trim();

        if (StringUtils.isEmpty(value)) {

            return null;

        }

        if (StringUtils.isNotEmpty(dateFormat)) {

            return parseDate(value, dateFormat);

        } else {

            return parseDate(value, ""yyyy-MM-dd HH:mm:ss"");

        }

    }



    /**

     * format date

     *

     * @param dateStr

     * @param format S

     * @return Date

     */

    public Date parseDate(String dateStr, String format) {

        Date date=null;

        try {

            // 这里可以根据dateStr 是时间戳还是日期格式字符串来做转换           

            DateFormat dateFormat = new SimpleDateFormat(format);

            date = dateFormat.parse(dateStr);

         } catch (Exception e) {

            logger.error("formatting date string {} is error", dateStr);

         }

        return date;

    }

}

接下来,我们来看看使用SpringMVC进行开发的时候,通过不同方式传递Date类型的请求参数具体原理是什么。SpringMVC中处理控制器参数的接口是HandlerMethodArgumentResolver,此接口有很多子类,分别处理不同注解的参数,上述提到了@RequestBody和@RequestParam两种注解,依次对应着两种解析处理器

  • RequestParamMethodArgumentResolver:解析处理使用了@RequestParam注解的参数、MultipartFile类型参数和java基本数据类型(如long、int、byte等)参数。
  • RequestResponseBodyMethodProcessor:解析处理@RequestBody注解的参数。

实际上,一般在解析一个控制器的请求参数,用到的是解析器组合对象:HandlerMethodArgumentResolverComposite,其封装了所有继承HandlerMethodArgumentResolver解析器的子类。而HandlerMethodArgumentResolver子类在解析参数的时候会用HttpMessageConverter转换器的子类进行数据匹配转换。常见的有MappingJackson2HttpMessageConverter,用来处理application/json媒体类型RequestResponseBodyMethodProcessor用来转换json字符串);FormHttpMessageConverter,用来处理form表单数据和application/x-www-form-urlencoded(RequestParamMethodArgumentResolver用来转换@RequestParam注解的参数和无注解的java基本数据类型参数其实HandlerMethodArgumentResolver子类使用哪个HttpMessageConverter子类实际上是由请求头中的ContentType决定的

首先,分析一下使用了@RequestParam注解和不带注解的情况。RequestParamMethodArgumentResolver接收数据之前,TypeConverterDelegate会通过DataBinder来对数据进行处理,这里可以在Controller中注册InitBinder对Date类型数据转换,如上述方法一。DataBinder在对数据进行转换后,会把数据交给RequestParamMethodArgumentResolver处理,此时,FormHttpMessageConverter会把数据交给TypeConverterDelegate处理,TypeConverterDelegate会通过会通过sourceType和targetType两种类型来查找Converter,如果此时定义了String数据类型转Date数据类型的Converter时,就会调用自定义的Converter,否则的系统会调用默认的转换器ObjectToObjectConverter。所以,可以自定义String数据类型转Date数据类型的Converter,实现String到Date的转换,如上述方法二。

其次,分析带有@RequestBody注解,Date单独作为接口参数的情况。在AbstractJackson2HttpMessageConverter中,会有一个ObjectMapper对象,该对象主要是负责json字符串的转换。AbstractJackson2HttpMessageConverter类中调用readJavaType方法,该方法调用objectMapper对象的readValue(InputStream src, JavaType valueType)方法来读取数据,其中,reader载有源数据,javaType表示需要转换成的数据类型。 接下来方法调用关系为: readValue->_readMapAndClose->_findRootDeserializer->DateDeserializer的deserialize方法->_parseDate->父类_parseDate(代码见示例6所示,转向case 7分支),通过JsonParser的getLongValue方法获取时间戳,然后创建Date对象返回。在这里,如果日期数据为时间戳,会被正常解析,如果日期数据为日期字符串(yyyy-MM-dd HH:mm:ss格式数据),方法调用关系为:readValue->_readMapAndClose->_initForReading->UTF8StreamJsonParser的nextToken方法->_nextTokenNotInObject->_parsePosNumber->_verifyRootSpace,在_verifyRootSpace方法中,如果出现json解析的非法字符,会调用_reportUnexpectedChar,并抛出异常,该方法代码如示例代码7所示。所以,当这里的javaType为Date类型时,源数据格式必须为时间戳,否则为抛出异常。

示例代码6


protected Date _parseDate(JsonParser p, DeserializationContext ctxt) throws IOException {
        switch(p.getCurrentTokenId()) {
        case 3:
            return this._parseDateFromArray(p, ctxt);
        case 4:
        case 5:
        case 8:
        case 9:
        case 10:
        default:
            return (Date)ctxt.handleUnexpectedToken(this._valueClass, p);
        case 6:
            return this._parseDate(p.getText().trim(), ctxt);
        case 7:
            long ts;
            try {
                ts = p.getLongValue();
            } catch (InputCoercionException | JsonParseException var7) {
                Number v = (Number)ctxt.handleWeirdNumberValue(this._valueClass, p.getNumberValue(), "not a valid 64-bit long for creating `java.util.Date`", new Object[0]);
                ts = v.longValue();
            }

            return new Date(ts);
        case 11:
            return (Date)this.getNullValue(ctxt);
        }
    }

 示例代码7

    protected void _reportUnexpectedChar(int ch, String comment) throws JsonParseException {

        if (ch < 0) {

            this._reportInvalidEOF();

        }



        String msg = String.format("Unexpected character (%s)", _getCharDesc(ch));

        if (comment != null) {

            msg = msg + ": " + comment;

        }



        this._reportError(msg);

    }

 

最后,分析带有@RequestBody注解,Date作为参数对象属性情况。AbstractJackson2HttpMessageConverter类中调用readJavaType方法,该方法调用objectMapper对象的readValue(reader, javaType)方法来读取数据,接着方法调用关系为:readValue->_readMapAndClose->JsonDeserializer对象的deserialize->deserializeFromObject方法。在deserializeFromObject方法中,SettableBeanProperty根据目标对象中属性的类型依次调用deserializeAndSet方法来对json数据中每一项数据进行转换。在方法deserializeAndSet中会调用数据类型对应的Deserializer(为StdDeserializer子类)对象中的deserialize方法。因此,在对Date类型转换时,调用了DateDeserializer对象的deserialize方法,接着方法调用的关系为:deserialize->_parseDate->父类_parseDate(代码如示例代码6,转向代码中的case 6分支)->StdDeserializer的 _parseDate(JsonParser p, DeserializationContext ctxt)->_parseDate(String value, DeserializationContext ctxt)->DeserializationContext的parseDate(String dateStr),在该方法中调用getDateFormat方法获取日期格式,这里会去配置文件中读取日期格式,就是在application.yml配置文件中配置spring:jackson:date-format的值。parseDate(String dateStr)方法的源码如示例代码8所示。

示例代码8

    public Date parseDate(String dateStr) throws IllegalArgumentException {

        try {

            DateFormat df = this.getDateFormat();

            return df.parse(dateStr);

        } catch (ParseException var3) {

            throw new IllegalArgumentException(String.format("Failed to parse Date value '%s': %s", dateStr, ClassUtil.exceptionMessage(var3)));

        }

    }

 

综上所述,就是Spring MVC中后端接收Date类型数据的来龙去脉。


版权声明:本文为liuhuanheng原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。