HandlerAdapter解析參數過程之HandlerMethodArgumentResolver RequestMappingHandlerAdapter和RequestParam原理分析

       在我們做Web開發的時候,會提交各種數據格式的請求,而我們的後臺也會有相應的參數處理方式。SpringMVC就爲我們提供了一系列的參數解析器,不管你是要獲取Cookie中的值,Header中的值,JSON格式的數據,URI中的值。下面我們分析幾個SpringMVC爲我們提供的參數解析器。

        在SpringMVC中爲我們定義了一個參數解析的頂級父類:HandlerMethodArgumentResolver。同時SpringMVC爲我們提供了這麼多的實現類:

這麼多的類,看起來眼花繚亂的。下面選擇幾個常用的參數解析的類型來分析一下。在這之前先說一下HandlerMethodArgumentResolverComposite這個類,這個類是SpringMVC參數解析器的一個集合,在RequestMappingHandlerAdapter中就定義了相關變量來處理參數。

接到上篇講的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類圖關係:

接着我們看一下resolveArgument的這個方法,我們在RequestParamMethodArgumentResolver這個方法中沒有找到對應的resolveArgument方法,但是我們在他的父類中找到了resolveArgument這個方法源碼如下:

 

    @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這個方法中看一下:

    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進行參數解析。

轉載地址https://blog.csdn.net/zknxx/article/details/78239951

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章