在咱們作Web開發的時候,會提交各類數據格式的請求,而咱們的後臺也會有相應的參數處理方式。SpringMVC就爲咱們提供了一系列的參數解析器,無論你是要獲取Cookie中的值,Header中的值,JSON格式的數據,URI中的值。下面咱們分析幾個SpringMVC爲咱們提供的參數解析器。html
在SpringMVC中爲咱們定義了一個參數解析的頂級父類:HandlerMethodArgumentResolver。同時SpringMVC爲咱們提供了這麼多的實現類:java
這麼多的類,看起來眼花繚亂的。下面選擇幾個經常使用的參數解析的類型來分析一下。在這以前先說一下HandlerMethodArgumentResolverComposite這個類,這個類是SpringMVC參數解析器的一個集合,在RequestMappingHandlerAdapter中就定義了相關變量來處理參數。web
接到上篇講的RequestMappingHandlerAdapter和RequestParam原理分析到getMethodArgumentValues方法中。後端
@RequestMapping("testindex") public String testIndex(Long id, String userName) { System.out.println(String.format("id:%d,userName:%s", id, userName)); return "/jsp/index"; }
這樣的寫法相信你們都很熟悉,那麼SpringMVC是怎麼解析出請求中的參數給InvocableHandlerMethod#doInvoke方法當入參的呢?通過咱們debug發現,這裏methodArgumentResolver.supportsParameter所匹配到的HandlerMethodArgumentResolver的實現類是RequestParamMethodArgumentResolver。咱們進入到RequestParamMethodArgumentResolver中看一下supportsParameter方法:數組
@Override public boolean supportsParameter(MethodParameter parameter) { //方法的參數中是否有RequestParam註解。 if (parameter.hasParameterAnnotation(RequestParam.class)) { //方法的參數是不是Map if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); return StringUtils.hasText(paramName); } else { return true; } } else { //若是有RequestPart註解,直接返回faslse if (parameter.hasParameterAnnotation(RequestPart.class)) { return false; } parameter = parameter.nestedIfOptional(); //是不是文件上傳中的值 if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true; } else if (this.useDefaultResolution) { //默認useDefaultResolution爲true return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false; } } }
在上面的代碼中咱們能夠看到,請求對應的處理方法的中的參數若是帶有RequestParam註解,則判斷是否是Map類型的參數,若是不是,則直接返回true。若是沒有RequestParam註解,則判斷若是有RequestPart註解,則直接返回false,接着判斷是不是文件上傳表單中的參數值,若是不是,則接着判斷useDefaultResolution是否爲true。這裏須要說明一下的是:argumentResolvers中有兩個RequestParamMethodArgumentResolver bean,一個useDefaultResolution爲false,一個useDefaultResolution爲true。當useDefaultResolution爲false的bean是用來處理RequestParam註解的,useDefaultResolution爲true的bean是用來處理簡單類型的bean的。在咱們這個例子中,useDefaultResolution的值爲true。那麼接下來回判斷是否是簡單類型參數,咱們進到BeanUtils.isSimpleProperty這個方法中看一下:緩存
public static boolean isSimpleProperty(Class<?> clazz) { Assert.notNull(clazz, "Class must not be null"); return isSimpleValueType(clazz) || (clazz.isArray() && isSimpleValueType(clazz.getComponentType())); }
public static boolean isSimpleValueType(Class<?> clazz) { return (ClassUtils.isPrimitiveOrWrapper(clazz) || clazz.isEnum() || CharSequence.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz) || URI.class == clazz || URL.class == clazz || Locale.class == clazz || Class.class == clazz); }
真正進行類型判斷的方法是isSimpleValueType這個方法,若是請求對應處理類的方法的參數爲枚舉類型、String類型、Long、Integer、Float、Byte、Short、Double、Date、URI、URL、Locale、Class、文件上傳對象或者參數是數組,數組類型爲上面列出的類型則返回true。即咱們的請求對應處理類的方法的參數爲:枚舉類型、String類型、Long、Integer、Float、Byte、Short、Double、Date、URI、URL、Locale、Class、文件上傳對象或者參數是數組,數組類型爲上面列出的類型,則請求參數處理類爲:RequestParamMethodArgumentResolver。咱們先看一下RequestParamMethodArgumentResolver的UML類圖關係:
app
接着咱們看一下resolveArgument的這個方法,咱們在RequestParamMethodArgumentResolver這個方法中沒有找到對應的resolveArgument方法,可是咱們在他的父類中找到了resolveArgument這個方法源碼以下:jsp
@Override public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { //獲取請求對應處理方法的參數字段值 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); //解析以後的請求對應處理方法的參數字段值 Object resolvedName = resolveStringValue(namedValueInfo.name); if (resolvedName == null) { throw new IllegalArgumentException( "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); } //解析參數值 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); //若是從請求中獲得的參數值爲null的話 if (arg == null) { //判斷是否有默認值 if (namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } //判斷這個字段是不是必填,RequestParam註解默認爲必填。 else if (namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } //處理null值 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } //若是爲獲得的參數值爲null,且有默認的值 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } //參數的校驗 if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } } //空實現 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }
在這個方法中,首先獲取到參數名字是什麼,這裏封裝爲了NamedValueInfo的一個對象,咱們能夠去getNamedValueInfo這個方法中看一下:ide
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { //先從緩存中獲取 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); if (namedValueInfo == null) { //建立NamedValueInfo對象 namedValueInfo = createNamedValueInfo(parameter); //更新剛纔獲得的NamedValueInfo對象 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); //放入緩存中 this.namedValueInfoCache.put(parameter, namedValueInfo); } return namedValueInfo; }
咱們看一下createNamedValueInfo這個方法,這個方法在RequestParamMethodArgumentResolver中:函數
@Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { //判斷是否有RequestParam註解 RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); }
RequestParamNamedValueInfo對象的源碼以下:
private static class RequestParamNamedValueInfo extends NamedValueInfo { public RequestParamNamedValueInfo() { super("", false, ValueConstants.DEFAULT_NONE); } public RequestParamNamedValueInfo(RequestParam annotation) { super(annotation.name(), annotation.required(), annotation.defaultValue()); } }
這兩個構造函數的區別是,若是有RequestParam註解的話,則取ReuqestParam註解中的值,不然取默認的值,咱們看一下updateNamedValueInfo這個方法的源碼:
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { String name = info.name; //若是上一步建立的NamedValueInfo中的name爲空的話 if (info.name.isEmpty()) { //從MethodParameter中解析出參數的名字 name = parameter.getParameterName(); if (name == null) { throw new IllegalArgumentException( "Name for argument type [" + parameter.getNestedParameterType().getName() + "] not available, and parameter name information not found in class file either."); } } //轉換默認值 String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); return new NamedValueInfo(name, info.required, defaultValue); }
這個方法的主要做用是獲取參數的名字。學過反射的咱們都知道經過反射的API只能獲取方法的形參的類型,不能獲取形參的名稱,可是這裏很明顯咱們須要獲取到形參的名稱,因此這裏獲取形參的名稱不是經過反射的方式獲取,而是經過了一個叫ASM的技術來實現的。當咱們獲取到NamedValueInfo 以後,會對獲取到的造成作進一步的處理,這裏咱們能夠先不用關注,直接到resolveName這個方法中看一下:
@Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { //獲取HttpServletRequest HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); //判斷是否是文件上傳的請求 MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); //先從文件上傳的請求中獲取上傳的文件對象(Part這種東西如今應該不多用了吧,因此這裏就直接忽略了) Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } Object arg = null; //若是是文件上傳請求, if (multipartRequest != null) { //則獲取文件上傳請求中的普通表單項的值 List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { //若是隻有一個值的話,則返回一個值,不然返回數組 arg = (files.size() == 1 ? files.get(0) : files); } } //說明是普通的請求 if (arg == null) { //則從request.getParameterValues中獲取值 String[] paramValues = request.getParameterValues(name); if (paramValues != null) { //若是隻有一個值的話,則返回一個值,不然返回數組 arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } return arg; }
這裏同時支持了獲取文件上傳對象、文件上傳請求中的表單項參數值的獲取,普通請求參數值的獲取。對於普通請求參數值的獲取是經過request.getParameterValues來獲取的。
若是咱們沒有從請求中獲取到參數值的話,則先判斷是不是有默認值(用RequestParam註解能夠設置默認值),接着判斷這個參數是不是必要的參數(RequestParam默認爲必要參數),若是是必要的參數且這個值爲null的話,則處理過程以下:
@Override protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { throw new MultipartException("Current request is not a multipart request"); } else { throw new MissingServletRequestPartException(name); } } else { throw new MissingServletRequestParameterException(name, parameter.getNestedParameterType().getSimpleName()); } }
若是是文件上傳請求,則異常信息爲:Current request is not a multipart request。普通請求,則異常信息爲:"Required " + this.parameterType + " parameter '" + this.parameterName + "' is not present"。若是值爲null的話,則會對null值進行處理,
private Object handleNullValue(String name, Object value, Class<?> paramType) { if (value == null) { if (Boolean.TYPE.equals(paramType)) { return Boolean.FALSE; } else if (paramType.isPrimitive()) { //int long 等 throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name + "' is present but cannot be translated into a null value due to being declared as a " + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); } } return value; }
若是參數類型爲int、long等基本類型,則若是請求參數值爲null的話,則會拋出異常,異常信息以下:"Optional " + paramType.getSimpleName() + " parameter '" + name +' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type."。
咱們在上一步獲取到的參數,會在下一步進行數據校驗。
若是咱們的請求換成這個:
http://localhost:8080/allRequestFormat/requestParamRequest?id=122
後臺處理代碼以下:
@RequestMapping("requestParamRequest") public String requestParamRequest(@RequestParam("id") Long id) { System.out.println("參數ID爲:" + id); return "這是一個帶RequestParam註解的請求"; }
這個處理過程,和咱們上面說的處理過程基本是一致的。
這裏再多說一些RequestParam這個註解,使用RequestParam註解的好處是,能夠指定所要的請求參數的名稱,縮短處理過程,能夠指定參數默認值。
另外再多說一點:java類文件編譯爲class文件時,有release和debug模式之分,在命令行中直接使用javac進行編譯的時候,默認的是release模式,使用release模式會改變形參中的參數名,若是造成的名稱變化的話,咱們可能不能正確的獲取到請求參數中的值。而IDE都是使用debug模式進行編譯的。ant編譯的時候,須要在ant的配置文件中指定debug="true"。 若是要修改javac編譯類文件的方式的話,須要指定-g參數。即:javac -g 類文件。
咱們最後再總結一下:若是你的請求對應處理類中的形參類型爲:枚舉類型、String類型、Long、Integer、Float、Byte、Short、Double、Date、URI、URL、Locale、Class、文件上傳對象或者參數是數組,數組類型爲上面列出的類型的話,則會使用RequestParamMethodArgumentResolver進行參數解析。