首先說明:以版本為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