SpringMvc 请求中日期类型参数接收一二事儿
首先说明:以版本为Spring 4.3.0为测试对象; 开启<mvc:annotation-driven />
测试场景一:请求中含有date属性,该类型为日期类型,SpringMvc采用@RequestParam来接受作为方法入参。
代码很简单,第一反应是不能将字符串的date属性赋给d;
先尝试输入当前日期 2019-02-21 20:30 并提交,当然现在大多都是前端日期控件来选择日期并按照一定类型提交到后台的;
@RequestMapping(value="/form9") @ResponseBody public String form9(@RequestParam(name="date") Date d) { //基本类型会转成包装类型,尝试转换 return "form9 Response Ok! " + d; }
查看报错信息: 没能够将字符串类型转换需要的日期类型
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.util.Date] for value '2019-02-21 20:30'; nested exception is java.lang.IllegalArgumentException at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:109) at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:36) at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192) at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:173) at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:108) at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:64) at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:47) at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:688)
其实,不是这样的,当输入日期为 2018/02/21 20:36:00 这样的,你会发现又可以将字符串转为日期类型
原因我DEBUG简单分析如下:因为@RequestParam注解决定了使用 RequestParamMethodArgumentResolver这个参数解析器,mvc-annotation注册的参数类型转换器 并没有
String—>Date类型的转换器 , 但是用到了ObjectToObjectConvert这个转换器;下图贴一下其 类型转换的convert 方法,就是尝试去寻找 目标类 Date 构造方法
即目标类构造方法需要唯一含有String类型的构造方法,然后实例化 该目标类, 没找到就会抛出异常;
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } Class<?> sourceClass = sourceType.getType(); Class<?> targetClass = targetType.getType(); Member member = getValidatedMember(targetClass, sourceClass); try { if (member instanceof Method) { Method method = (Method) member; ReflectionUtils.makeAccessible(method); if (!Modifier.isStatic(method.getModifiers())) { return method.invoke(source); } else { return method.invoke(null, source); } } else if (member instanceof Constructor) { Constructor<?> ctor = (Constructor<?>) member; return ctor.newInstance(source); } } catch (InvocationTargetException ex) { throw new ConversionFailedException(sourceType, targetType, source, ex.getTargetException()); } catch (Throwable ex) { throw new ConversionFailedException(sourceType, targetType, source, ex); } // If sourceClass is Number and targetClass is Integer, the following message should expand to: // No toInteger() method exists on java.lang.Number, and no static valueOf/of/from(java.lang.Number) // method or Integer(java.lang.Number) constructor exists on java.lang.Integer. throw new IllegalStateException(String.format("No to%3$s() method exists on %1$s, " + "and no static valueOf/of/from(%1$s) method or %3$s(%1$s) constructor exists on %2$s.", sourceClass.getName(), targetClass.getName(), targetClass.getSimpleName())); }
因为Date有个虽然过时、但是确实是String类的构造方法:至于parse方法就是将String字符串转为long类型,支持格式有常见的几种:
2018/02/21 20:36:00 或者 2019/02/21 或者 02/21/2019 20:58:00
具体看是否支持你传过来的日期格式,new Date(“your pattern”)是否抛出异常即可
@Deprecated public Date(String s) { this(parse(s)); }
为了验证我的说法,改动下一些地方:接收参数改成自定义MyDate对象,其中name属性只是为了指定将名字为 date 的参数传递给MyDate的惟一的String构造方法;
@RequestMapping(value="/form9") @ResponseBody public String form9(@RequestParam(name="date") MyDate d) { //基本类型会转成包装类型,尝试转换 return "form9 Response Ok! " + d; } public class MyDate { public MyDate(String str) throws ParseException { System.out.println("调用MyDate构造器"); this.date = new SimpleDateFormat("yyyy-MM-dd").parse(str); } private Date date; @Override public String toString() { return "MyDate{" +"date=" + date +'}'; } }
测试结果呢? 证明 SpringMvc mvc-annotation开启,也是能够将字符串类型用Date类型接收,只是接收方式就是调用Date的一个参数String类型的构造器转换成Date类型;
测试场景二.@DateTimeFormat 接收自定义日期格式
说明:使用@DateTimeFormat,在<mvc:annotation-driven />基础上,Spring版本4.3.0
有两种方式:方式一,只需要当前项目编译为JDK1.7、1.8以及更高版本,会自动支持@DateTimeFormat注解;
方式二,低版本的话需要引入 joda-time jar包;(看到网上有种说法需要引入该包才能支持@DateTimeFormat,觉得片面了,JDK1.7以及以上可以不添加该jar包);
至于@DateTimeFormat用法有两种:
方式一: 结合@RequestParam
@RequestMapping(value="/form9") @ResponseBody public String form9(@RequestParam(name="date") @DateTimeFormat(pattern = "MM-yyyy-dd") Date d) { return "form9 Response Ok! " + d; }
方式二: SpringMvc接收参数为自定义Java对象,在自定义的Java对象属性上标注@DateTimeFormat注解;
(重要的是这种情况下Java对象必须要有空参的构造器,以及对应日期属性的set方法,没有构造器就抛出异常无法实例化Java对象,没有set方法就是Java对象属性为null)
@RequestMapping(value="/form8") @ResponseBody public String form8(TimePojo pojo) { return "form8 Response Ok! " + pojo; } @Setter @Getter @ToString //节约篇幅,setter、getter、ToString来自lombok public class TimePojo { @DateTimeFormat(pattern = "yyyy-mm-dd") private Date date; }
下面花一些篇幅记录下,我Debug过程中分析的两种方式的异同:
方式一而言,参数解析器之前提过,RequestParamMethodArgumentResolver这个解析器来解析@RequestParam参数这点是不变的, 转换器之前迫不得已找的ObjectToObjectConverter,
这次使用到的Converter,是AnnotationParserConverter,其专门针对String—>@DateTimeFormat的转换器, 这些转换器都注册在SpringMvc的ConversionService中;
看下AnnotationParserConverter的convert方法,最后几行调用了ParserConvert的convert方法,ParserConvert继承自GenericConvert,其convert方法就是使用它的
parser对象的parse方法,当前情况也就是DateFormatter的parser方法, 下图可以看到其parse方法等同于new SimpleDateFormat(“patten”).parse(“..”);
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
//当前情况是 获取@DateTimeFormat注解 Annotation ann = targetType.getAnnotation(this.annotationType); if (ann == null) { throw new IllegalStateException( "Expected [" + this.annotationType.getName() + "] to be present on " + targetType); } AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType()); GenericConverter converter = cachedParsers.get(converterKey); if (converter == null) { Parser<?> parser = this.annotationFormatterFactory.getParser( converterKey.getAnnotation(), converterKey.getFieldType());
//当前情况annotationFormatterFactory为DateTimeFormatAnnotationFormatterFactory
//获取到的parser对象是 DateFormatter
converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this); cachedParsers.put(converterKey, converter); } return converter.convert(source, sourceType, targetType); }
到这里也就完成了请求request中的String类型参数赋给方法Date类型入参,你以为到这里就结束了,看下面两种情况也是同样可以接受并转换参数的!
补充说明:之前DateFormatter返回了Date类型参数,下图是ParserConvert的convert方法,注意到paser完成以后, Date不符合需要参数Calendar类型要求,于是继续调用conversionService的convert进行解析,正好SpringMvc <mvc:annotation-driven/>替我们注册了Date->Calendar的转换器,转换的方法也非常简单,
Calendar calendar = Calendar.getInstance();calendar.setTime(source); Long类型的转换器也是类似的因为SpringMvc注册的Date->Long的转换器, date.getTime()即完成转换;
方式二而言,参数解析器是ServletModelAttributeMethodProcessor,作用就是将请求参数映射到Java对象的属性上;重要的一点,该Java对象需要有空参public类型构造器,不然无法实例化抛出异常,这种方式也支持级联属性设置,同样的级联的属性也需要有空参构造方法; 看到过网上有个笔记 lombok的@Builder注解会使这种方式接受请求参数映射到属性失效,因为生成的class文件破坏了默认构造器,就是没有空的构造方法了;
扯得有点远了,关于日期类型总结下几点:
不管SpringMvc是否默认支持将请求中字符串转为Date类型,最轻松的方式应该就是使用@DateFormat注解,1.7以及更高的1.8版本直接就可以使用,其他还有方式比如自定义conversion-service