HandlerMethodArgumentResolver(一):Controller方法入?yún)⒆詣?dòng)封裝器【享學(xué)Spring MVC】

每篇一句

你的工作效率高,老板會(huì)認(rèn)為你強(qiáng)度不夠。你代碼bug多,各種生產(chǎn)環(huán)境救火,老板會(huì)覺得你是團(tuán)隊(duì)的核心成員。

前言

在享受Spring MVC帶給你便捷的時(shí)候,你是否曾經(jīng)這樣疑問過:Controllerhandler方法參數(shù)能夠自動(dòng)完成封裝(有時(shí)即使沒有@PathVariable、@RequestParam、@RequestBody等注解都可),甚至在方法參數(shù)任意位置寫HttpServletRequest、HttpSession、Writer...等類型的參數(shù),它自動(dòng)就有值了便可直接使用。
對(duì)此你是否想問一句:Spring MVC它是怎么辦到的?那么本文就揭開它的神秘面紗,還你一片"清白"。

Spring MVC作為一個(gè)最為流行的web框架,早早已經(jīng)成為了實(shí)際意義上的標(biāo)準(zhǔn)化(框架),特別是隨著Struts2的突然崩塌,Spring MVC幾乎一騎絕塵,因此深入了解它有著深遠(yuǎn)的意義

Spring MVC它只需要區(qū)區(qū)幾個(gè)注解就能夠讓一個(gè)普通的java方法成為一個(gè)Handler處理器,并且還能有自動(dòng)參數(shù)封裝、返回值視圖處理/渲染等一系列強(qiáng)大功能,讓coder的精力更加的聚焦在自己的業(yè)務(wù)。

像JSF、Google Web Toolkit、Grails Framework等web框架至少我是沒有用過的。
這里有個(gè)輕量級(jí)的web框架:Play Framework設(shè)計(jì)上我個(gè)人覺得還挺有意思,有興趣的可以玩玩

HandlerMethodArgumentResolver

策略接口:用于在給定請(qǐng)求的上下文中將方法參數(shù)解析為參數(shù)值。簡單的理解為:它負(fù)責(zé)處理你Handler方法里的所有入?yún)?/strong>:包括自動(dòng)封裝、自動(dòng)賦值、校驗(yàn)等等。有了它才能會(huì)讓Spring MVC處理入?yún)@得那么高級(jí)、那么自動(dòng)化。
Spring MVC內(nèi)置了非常非常多的實(shí)現(xiàn),當(dāng)然若還不能滿足你的需求,你依舊可以自定義和自己注冊(cè),后面我會(huì)給出自定義的示例。

有個(gè)形象的公式:HandlerMethodArgumentResolver = HandlerMethod + Argument(參數(shù)) + Resolver(解析器)
解釋為:它是HandlerMethod方法的解析器,將HttpServletRequest(header + body 中的內(nèi)容)解析為HandlerMethod方法的參數(shù)(method parameters)

// @since 3.1   HandlerMethod 方法中 參數(shù)解析器
public interface HandlerMethodArgumentResolver {

    // 判斷 HandlerMethodArgumentResolver 是否支持 MethodParameter
    // (PS: 一般都是通過 參數(shù)上面的注解|參數(shù)的類型)
    boolean supportsParameter(MethodParameter parameter);
    
    // 從NativeWebRequest中獲取數(shù)據(jù),ModelAndViewContainer用來提供訪問Model
    // MethodParameter parameter:請(qǐng)求參數(shù)
    // WebDataBinderFactory用于創(chuàng)建一個(gè)WebDataBinder用于數(shù)據(jù)綁定、校驗(yàn)
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

基于這個(gè)接口的處理器實(shí)現(xiàn)類不可謂不豐富,非常之多。我截圖如下:


在這里插入圖片描述

因?yàn)樽宇惐姸?,所以我分類進(jìn)行說明。我把它分為四類進(jìn)行描述:

  1. 基于Name
  2. 數(shù)據(jù)類型是Map
  3. 固定參數(shù)類型
  4. 基于ContentType的消息轉(zhuǎn)換器

第一類:基于Name

從URI(路徑變量)、HttpServletRequest、HttpSession、Header、Cookie...等中根據(jù)名稱key來獲取值

這類處理器所有的都是基于抽象類AbstractNamedValueMethodArgumentResolver來實(shí)現(xiàn),它是最為重要的分支(分類)。

// @since 3.1  負(fù)責(zé)從路徑變量、請(qǐng)求、頭等中拿到值。(都可以指定name、required、默認(rèn)值等屬性)
// 子類需要做如下事:獲取方法參數(shù)的命名值信息、將名稱解析為參數(shù)值
// 當(dāng)需要參數(shù)值時(shí)處理缺少的參數(shù)值、可選地處理解析值

//特別注意的是:默認(rèn)值可以使用${}占位符,或者SpEL語句#{}是木有問題的
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Nullable
    private final ConfigurableBeanFactory configurableBeanFactory;
    @Nullable
    private final BeanExpressionContext expressionContext;
    private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);

    public AbstractNamedValueMethodArgumentResolver() {
        this.configurableBeanFactory = null;
        this.expressionContext = null;
    }
    public AbstractNamedValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
        this.configurableBeanFactory = beanFactory;
        // 默認(rèn)是RequestScope
        this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);
    }

    // protected的內(nèi)部類  所以所有子類(注解)都是用友這三個(gè)屬性值的
    protected static class NamedValueInfo {
        private final String name;
        private final boolean required;
        @Nullable
        private final String defaultValue;
        public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
            this.name = name;
            this.required = required;
            this.defaultValue = defaultValue;
        }
    }

    // 核心方法  注意此方法是final的,并不希望子類覆蓋掉他~
    @Override
    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        // 創(chuàng)建 MethodParameter 對(duì)應(yīng)的 NamedValueInfo
        NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
        // 支持到了Java 8 中支持的 java.util.Optional
        MethodParameter nestedParameter = parameter.nestedIfOptional();

        // name屬性(也就是注解標(biāo)注的value/name屬性)這里既會(huì)解析占位符,還會(huì)解析SpEL表達(dá)式,非常強(qiáng)大
        // 因?yàn)榇藭r(shí)的 name 可能還是被 ${} 符號(hào)包裹, 則通過 BeanExpressionResolver 來進(jìn)行解析
        Object resolvedName = resolveStringValue(namedValueInfo.name);
        if (resolvedName == null) {
            throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        }


        // 模版抽象方法:將給定的參數(shù)類型和值名稱解析為參數(shù)值。  由子類去實(shí)現(xiàn)
        // @PathVariable     --> 通過對(duì)uri解析后得到的decodedUriVariables值(常用)
        // @RequestParam     --> 通過 HttpServletRequest.getParameterValues(name) 獲?。ǔS茫?        // @RequestAttribute --> 通過 HttpServletRequest.getAttribute(name) 獲取   <-- 這里的 scope 是 request
        // @SessionAttribute --> 略
        // @RequestHeader    --> 通過 HttpServletRequest.getHeaderValues(name) 獲取
        // @CookieValue      --> 通過 HttpServletRequest.getCookies() 獲取
        Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);

        // 若解析出來值仍舊為null,那就走defaultValue (若指定了的話)
        if (arg == null) {
            // 可以發(fā)現(xiàn):defaultValue也是支持占位符和SpEL的~~~
            if (namedValueInfo.defaultValue != null) {
                arg = resolveStringValue(namedValueInfo.defaultValue);

            // 若 arg == null && defaultValue == null && 非 optional 類型的參數(shù) 則通過 handleMissingValue 來進(jìn)行處理, 一般是報(bào)異常
            } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                
                // 它是個(gè)protected方法,默認(rèn)拋出ServletRequestBindingException異常
                // 各子類都復(fù)寫了此方法,轉(zhuǎn)而拋出自己的異常(但都是ServletRequestBindingException的異常子類)
                handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
            }
    
            // handleNullValue是private方法,來處理null值
            // 針對(duì)Bool類型有這個(gè)判斷:Boolean.TYPE.equals(paramType) 就return Boolean.FALSE;
            // 此處注意:Boolean.TYPE = Class.getPrimitiveClass("boolean") 它指的基本類型的boolean,而不是Boolean類型哦~~~
            // 如果到了這一步(value是null),但你還是基本類型,那就拋出異常了(只有boolean類型不會(huì)拋異常哦~)
            // 這里多嘴一句,即使請(qǐng)求傳值為&bool=1,效果同bool=true的(1:true 0:false) 并且不區(qū)分大小寫哦(TrUe效果同true)
            arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
        }
        // 兼容空串,若傳入的是空串,依舊還是使用默認(rèn)值(默認(rèn)值支持占位符和SpEL)
        else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
            arg = resolveStringValue(namedValueInfo.defaultValue);
        }

        // 完成自動(dòng)化的數(shù)據(jù)綁定~~~
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            try {
                // 通過數(shù)據(jù)綁定器里的Converter轉(zhuǎn)換器把a(bǔ)rg轉(zhuǎn)換為指定類型的數(shù)值
                arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
            } catch (ConversionNotSupportedException ex) { // 注意這個(gè)異常:MethodArgumentConversionNotSupportedException  類型不匹配的異常
                throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());
            } catch (TypeMismatchException ex) { //MethodArgumentTypeMismatchException是TypeMismatchException 的子類
                throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());

            }
        }

        // protected的方法,本類為空實(shí)現(xiàn),交給子類去復(fù)寫(并不是必須的)
        // 唯獨(dú)只有PathVariableMethodArgumentResolver把解析處理啊的值存儲(chǔ)一下數(shù)據(jù)到 
        // HttpServletRequest.setAttribute中(若key已經(jīng)存在也不會(huì)存儲(chǔ)了)
        handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
        return arg;
    }


    // 此處有緩存,記錄下每一個(gè)MethodParameter對(duì)象   value是NamedValueInfo值
    private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
        NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
        if (namedValueInfo == null) {
            // createNamedValueInfo是抽象方法,子類必須實(shí)現(xiàn)
            namedValueInfo = createNamedValueInfo(parameter);
            // updateNamedValueInfo:這一步就是我們之前說過的為何Spring MVC可以根據(jù)參數(shù)名封裝的方法
            // 如果info.name.isEmpty()的話(注解里沒指定名稱),就通過`parameter.getParameterName()`去獲取參數(shù)名~
            // 它還會(huì)處理注解指定的defaultValue:`\n\t\.....`等等都會(huì)被當(dāng)作null處理
            // 都處理好后:new NamedValueInfo(name, info.required, defaultValue);(相當(dāng)于吧注解解析成了此對(duì)象嘛~~)
            namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
            this.namedValueInfoCache.put(parameter, namedValueInfo);
        }
        return namedValueInfo;
    }

    // 抽象方法 
    protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
    // 由子類根據(jù)名稱,去把值拿出來
    protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception;
}

該抽象類中定義了解析參數(shù)的主邏輯(模版邏輯),子類只需要實(shí)現(xiàn)對(duì)應(yīng)的抽象模版方法即可。
對(duì)此部分的處理步驟,我把它簡述如下:

  1. 基于MethodParameter構(gòu)建NameValueInfo <-- 主要有name, defaultValue, required(其實(shí)主要是解析方法參數(shù)上標(biāo)注的注解~)
  2. 通過BeanExpressionResolver(${}占位符以及SpEL) 解析name
  3. 通過模版方法resolveNameHttpServletRequest, Http Headers, URI template variables 等等中獲取對(duì)應(yīng)的屬性值(具體由子類去實(shí)現(xiàn))
  4. 對(duì) arg==null這種情況的處理, 要么使用默認(rèn)值, 若 required = true && arg == null, 則一般報(bào)出異常(boolean類型除外~)
  5. 通過WebDataBinderarg轉(zhuǎn)換成Methodparameter.getParameterType()類型(注意:這里僅僅只是用了數(shù)據(jù)轉(zhuǎn)換而已,并沒有用bind()方法)

該抽象類繼承樹如下:

在這里插入圖片描述

從上源碼可以看出,抽象類已經(jīng)定死了處理模版(方法為final的),留給子類需要做的事就不多了,大體還有如下三件事:

  1. 根據(jù)MethodParameter創(chuàng)建NameValueInfo(子類的實(shí)現(xiàn)可繼承自NameValueInfo,就是對(duì)應(yīng)注解的屬性們)
  2. 根據(jù)方法參數(shù)名稱nameHttpServletRequest, Http Headers, URI template variables等等中獲取屬性值
  3. 對(duì)arg == null這種情況的處理(非必須)

PathVariableMethodArgumentResolver

它幫助Spring MVC實(shí)現(xiàn)restful風(fēng)格的URL。它用于處理標(biāo)注有@PathVariable注解的方法參數(shù),用于從URL中獲取值(并不是?后面的參數(shù)哦)。
并且,并且,并且它還可以解析@PathVariable注解的value值不為空的Map(使用較少,個(gè)人不太建議使用)~



UriComponentsContributor接口:通過查看方法參數(shù)和參數(shù)值并決定應(yīng)更新目標(biāo)URL的哪個(gè)部分,為構(gòu)建UriComponents的策略接口。

// @since 4.0 出現(xiàn)得還是比較晚的
public interface UriComponentsContributor {

    // 此方法完全同HandlerMethodArgumentResolver的這個(gè)方法~~~
    boolean supportsParameter(MethodParameter parameter);
    // 處理給定的方法參數(shù),然后更新UriComponentsbuilder,或者使用uri變量添加到映射中,以便在處理完所有參數(shù)后用于擴(kuò)展uri~~~
    void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder,
            Map<String, Object> uriVariables, ConversionService conversionService);
}

它的三個(gè)實(shí)現(xiàn)類:


在這里插入圖片描述

關(guān)于此接口的使用,后面再重點(diǎn)介紹,此處建議自動(dòng)選擇性忽略。



// @since 3.0 需要注意的是:它只支持標(biāo)注在@RequestMapping的方法(處理器)上使用~
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";
    @AliasFor("value")
    String name() default "";
    
    // 注意:它并沒有defaultValue哦~

    // @since 4.3.3  它也是標(biāo)記為false非必須的~~~~
    boolean required() default true;
}

// @since 3.1
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {
    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);


    // 簡單一句話描述:@PathVariable是必須,不管你啥類型
    // 標(biāo)注了注解,且是Map類型,
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (!parameter.hasParameterAnnotation(PathVariable.class)) {
            return false;
        }
        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
            PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
            return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
        }
        return true;
    }

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
        return new PathVariableNamedValueInfo(ann);
    }
    private static class PathVariableNamedValueInfo extends NamedValueInfo {
        public PathVariableNamedValueInfo(PathVariable annotation) {
            // 默認(rèn)值使用的DEFAULT_NONE~~~
            super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
        }
    }

    // 根據(jù)name去拿值的過程非常之簡單,但是它和前面的只知識(shí)是有關(guān)聯(lián)的
    // 至于這個(gè)attr是什么時(shí)候放進(jìn)去的,AbstractHandlerMethodMapping.handleMatch()匹配處理器方法上
    // 通過UrlPathHelper.decodePathVariables() 把參數(shù)提取出來了,然后放進(jìn)request屬性上暫存了~~~
    // 關(guān)于HandlerMapping內(nèi)容,可來這里:https://blog.csdn.net/f641385712/article/details/89810020
    @Override
    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
    }

    // MissingPathVariableException是ServletRequestBindingException的子類
    @Override
    protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
        throw new MissingPathVariableException(name, parameter);
    }


    // 值完全處理結(jié)束后,把處理好的值放進(jìn)請(qǐng)求域,方便view里渲染時(shí)候使用~
    // 抽象父類的handleResolvedValue方法,只有它復(fù)寫了~
    @Override
    @SuppressWarnings("unchecked")
    protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {

        String key = View.PATH_VARIABLES;
        int scope = RequestAttributes.SCOPE_REQUEST;
        Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
        if (pathVars == null) {
            pathVars = new HashMap<>();
            request.setAttribute(key, pathVars, scope);
        }
        pathVars.put(name, arg);
    }
    ...
}

關(guān)于@PathVariable的使用,不用再給例子了。
唯一需要說一下如果類型是Map類型的情況下的使用注意事項(xiàng),如下:

@PathVariable("jsonStr") Map<String,Object> map

希望把jsonStr對(duì)應(yīng)的字符串解析成鍵值對(duì)封裝進(jìn)Map里。那么你必須,必須,必須注冊(cè)了能處理此字符串的Converter/PropertyEditor(自定義)。使用起來相對(duì)麻煩,但技術(shù)隱蔽性高。我一般不建議這么來用~


關(guān)于@PathVariable的required=false使用注意事項(xiàng)

這個(gè)功能是很多人比較疑問的,如何使用???

@ResponseBody
@GetMapping("/test/{id}")
public Person test(@PathVariable(required = false) Integer id) { ... }

以為這樣寫通過/test這個(gè)url就能訪問到了,其實(shí)這樣是不行的,會(huì)404。
正確姿勢(shì):

@ResponseBody
@GetMapping({"/test/{id}", "/test"})
public Person test(@PathVariable(required = false) Integer id) { ... }

這樣/test/test/1這兩個(gè)url就都能正常work了~

@PathVariable的required=false使用較少,一般用于在用URL傳多個(gè)值時(shí),但有些值是非必傳的時(shí)候使用。比如這樣的URL:"/user/{id}/{name}","/user/{id}","/user"


RequestParamMethodArgumentResolver

顧名思義,是解析標(biāo)注有@RequestParam的方法入?yún)⒔馕銎?,這個(gè)注解比上面的注解強(qiáng)大很多了,它用于從請(qǐng)求參數(shù)(?后面的)中獲取值完成封裝。這是我們的絕大多數(shù)使用場(chǎng)景。除此之外,它還支持MultipartFile,也就是說能夠從MultipartHttpServletRequest | HttpServletRequest 獲取數(shù)據(jù),并且并且并且還兜底處理沒有標(biāo)注任何注解的“簡單類型”~

// @since 2.5
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";
     // @since 4.2
    @AliasFor("value")
    String name() default "";
    boolean required() default true;
    String defaultValue() default ValueConstants.DEFAULT_NONE;
}
// @since 3.1
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {

    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);

    // 這個(gè)參數(shù)老重要了:
    // true:表示參數(shù)類型是基本類型 參考BeanUtils#isSimpleProperty(什么Enum、Number、Date、URL、包裝類型、以上類型的數(shù)組類型等等)
    // 如果是基本類型,即使你不寫@RequestParam注解,它也是會(huì)走進(jìn)來處理的~~~(這個(gè)@PathVariable可不會(huì)喲~)
    // fasle:除上以外的。  要想它處理就必須標(biāo)注注解才行哦,比如List等~
    // 默認(rèn)值是false
    private final boolean useDefaultResolution;

    // 此構(gòu)造只有`MvcUriComponentsBuilder`調(diào)用了  傳入的false
    public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
        this.useDefaultResolution = useDefaultResolution;
    }
    // 傳入了ConfigurableBeanFactory ,所以它支持處理占位符${...} 并且支持SpEL了
    // 此構(gòu)造都在RequestMappingHandlerAdapter里調(diào)用,最后都會(huì)傳入true來Catch-all Case  這種設(shè)計(jì)挺有意思的
    public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
        super(beanFactory);
        this.useDefaultResolution = useDefaultResolution;
    }

    // 此處理器能處理如下Case:
    // 1、所有標(biāo)注有@RequestParam注解的類型(非Map)/ 注解指定了value值的Map類型(自己提供轉(zhuǎn)換器哦)
    // ======下面都表示沒有標(biāo)注@RequestParam注解了的=======
    // 1、不能標(biāo)注有@RequestPart注解,否則直接不處理了
    // 2、是上傳的request:isMultipartArgument() = true(MultipartFile類型或者對(duì)應(yīng)的集合/數(shù)組類型  或者javax.servlet.http.Part對(duì)應(yīng)結(jié)合/數(shù)組類型)
    // 3、useDefaultResolution=true情況下,"基本類型"也會(huì)處理
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
                return (requestParam != null && StringUtils.hasText(requestParam.name()));
            } else {
                return true;
            }
        } else {
            if (parameter.hasParameterAnnotation(RequestPart.class)) {
                return false;
            }
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            } else if (this.useDefaultResolution) {
                return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
            } else {
                return false;
            }
        }
    }


    // 從這也可以看出:即使木有@RequestParam注解,也是可以創(chuàng)建出一個(gè)NamedValueInfo來的
    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
        return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
    }


    // 核心方法:根據(jù)Name 獲取值(普通/文件上傳)
    // 并且還有集合、數(shù)組等情況
    @Override
    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

        // 這塊解析出來的是個(gè)MultipartFile或者其集合/數(shù)組
        if (servletRequest != null) {
            Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return mpArg;
            }
        }

        Object arg = null;
        MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
        if (multipartRequest != null) {
            List<MultipartFile> files = multipartRequest.getFiles(name);
            if (!files.isEmpty()) {
                arg = (files.size() == 1 ? files.get(0) : files);
            }
        }

        // 若解析出來值仍舊為null,那處理完文件上傳里木有,那就去參數(shù)里取吧
        // 由此可見:文件上傳的優(yōu)先級(jí)是高于請(qǐng)求參數(shù)的
        if (arg == null) {
        
            //小知識(shí)點(diǎn):getParameter()其實(shí)本質(zhì)是getParameterNames()[0]的效果
            // 強(qiáng)調(diào)一遍:?ids=1,2,3 結(jié)果是["1,2,3"](兼容方式,不建議使用。注意:只能是逗號(hào)分隔)
            // ?ids=1&ids=2&ids=3  結(jié)果是[1,2,3](標(biāo)準(zhǔn)的傳值方式,建議使用)
            // 但是Spring MVC這兩種都能用List接收  請(qǐng)務(wù)必注意他們的區(qū)別~~~
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }
        return arg;
    }
    ...
}

可以看到這個(gè)ArgumentResolver處理器還是很強(qiáng)大的:不僅能處理標(biāo)注了@RequestParam的參數(shù),還能接收文件上傳參數(shù)。甚至那些你平時(shí)使用中不標(biāo)注該注解的封裝也是它來兜底完成的。至于它如何兜底的,可以參見下面這個(gè)騷操作:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    ...
    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
        // Annotation-based argument resolution
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
        ...
        // Catch-all  兜底
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
        resolvers.add(new ServletModelAttributeMethodProcessor(true));

        return resolvers;
    }
    ...
}

可以看到ServletModelAttributeMethodProcessorRequestParamMethodArgumentResolver一樣,也是有兜底的效果的。




在本文末,我搜集了一些自己使用過程中的一些疑惑進(jìn)行解惑,希望也一樣能幫助你豁然開朗。

get請(qǐng)求如何傳值數(shù)組、集合(List)

如題的這個(gè)case太常見了有木有,我們經(jīng)常會(huì)遇到使用get請(qǐng)求向后端需要傳值的需求(比如根據(jù)ids批量查詢)。但到底如何傳,URL怎么寫,應(yīng)該是有傻傻分不清楚的不確定的情況。

@PathVariable傳參
    @ResponseBody
    @GetMapping("/test/{objects}")
    public Object test(@PathVariable List<Object> objects) {
        System.out.println(objects);
        return objects;
    }

請(qǐng)求URL:/test/fsx,fsx,fsx??刂婆_(tái)打?。?/p>

[fsx, fsx, fsx]

集合接收成功(使用@PathVariable Object[] objects也是可以正常接收的)。
使用時(shí)應(yīng)注意如下兩點(diǎn):

  1. 多個(gè)值只能使用,號(hào)分隔才行(否則會(huì)被當(dāng)作一個(gè)值,放進(jìn)數(shù)組/集合里,不會(huì)報(bào)錯(cuò))
  2. @PathVariable注解是必須的。否則會(huì)交給ServletModelAttributeMethodProcessor兜底去處理,它要求有空構(gòu)造所以反射創(chuàng)建實(shí)例會(huì)報(bào)錯(cuò)(數(shù)組/List)。(注意:如果是這樣寫ArrayList<Object> objects,那是不會(huì)報(bào)錯(cuò)的,只是值肯定是封裝不進(jìn)來的,一個(gè)空對(duì)象而已)

說明:為何逗號(hào)分隔的String類型默認(rèn)就能轉(zhuǎn)化為數(shù)組,集合。請(qǐng)參考StringToCollectionConverter/StringToArrayConverter這種內(nèi)置的GenericConverter通用轉(zhuǎn)換器~~

@RequestParam傳參
    @ResponseBody
    @GetMapping("/test")
    public Object test(@RequestParam List<Object> objects) {
        System.out.println(objects);
        return objects;
    }

請(qǐng)求URL:/test/?objects=1,2,3??刂婆_(tái)打印:

[1, 2, 3]

請(qǐng)求URL改為:/test/?objects=1&objects=2&objects=3??刂婆_(tái)打?。?/p>

[1, 2, 3]

兩個(gè)請(qǐng)求的URL不一樣,但都能正確的達(dá)到效果。(@RequestParam Object[] objects這么寫兩種URL也能正常封裝)

對(duì)此有如下這個(gè)細(xì)節(jié)你必須得注意:對(duì)于集合List而言@RequestParam注解是必須存在的,否則報(bào)錯(cuò)如下(因?yàn)榻唤o兜底處理了):

在這里插入圖片描述

但如果你這么寫String[] objects即使不寫注解,也能夠正常完成正確封裝

說明:Object[] objects這么寫的話不寫注解是不行的(報(bào)錯(cuò)如上)。至于原因,各位小伙伴可以自行思考,沒想明白的話可以給我留言(建議小伙伴一定要弄明白緣由)~


PS:需要注意的是,Spring MVC的這么多HandlerMethodArgumentResolver它的解析是有順序的:如果多個(gè)HandlerMethodArgumentResolver都可以解析某一種類型,以順序在前面的先解析(后面的就不會(huì)再執(zhí)行解析了)。

源碼參考處:HandlerMethodArgumentResolverComposite.getArgumentResolver(MethodParameter parameter);

由于RequestParamMethodArgumentResolver同樣可以對(duì)Multipart文件上傳進(jìn)行解析,并且默認(rèn)順序在RequestPartMethodArgumentResolver之前,所以如果不添加@RequestPart注解,Multipart類型的參數(shù)會(huì)被RequestParamMethodArgumentResolver解析。


總結(jié)

本文是你理解Spring MVC強(qiáng)大的自動(dòng)數(shù)據(jù)封裝功能非常重要的一篇文章。它介紹了HandlerMethodArgumentResolver的功能和基本使用,以及深入介紹了最為重要的兩個(gè)注解@PathVariable@RequestParam以及各自對(duì)應(yīng)的ArgumentResolver處理器。
由于這個(gè)體系龐大,所以我會(huì)分多個(gè)章節(jié)進(jìn)行描述,歡迎訂閱和持續(xù)關(guān)注~

相關(guān)閱讀

【小家Spring】Spring MVC容器的web九大組件之---HandlerMapping源碼詳解(二)---RequestMappingHandlerMapping系列

HandlerMethodArgumentResolver(一):Controller方法入?yún)⒆詣?dòng)封裝器(將參數(shù)parameter解析為值)【享學(xué)Spring MVC】
HandlerMethodArgumentResolver(二):Map參數(shù)類型和固定參數(shù)類型【享學(xué)Spring MVC】
HandlerMethodArgumentResolver(三):基于HttpMessageConverter消息轉(zhuǎn)換器的參數(shù)處理器【享學(xué)Spring MVC】

知識(shí)交流

==The last:如果覺得本文對(duì)你有幫助,不妨點(diǎn)個(gè)贊唄。當(dāng)然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==

若對(duì)技術(shù)內(nèi)容感興趣可以加入wx群交流:Java高工、架構(gòu)師3群。
若群二維碼失效,請(qǐng)加wx號(hào):fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會(huì)手動(dòng)邀請(qǐng)入群

==若對(duì)Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動(dòng)邀請(qǐng)你入群一起飛==

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容