系统定制开发前端向后端传递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中后端接收型数据的来龙去脉。