每篇一句
你的工作效率高,老板會(huì)認(rèn)為你強(qiáng)度不夠。你代碼bug多,各種生產(chǎn)環(huán)境救火,老板會(huì)覺得你是團(tuán)隊(duì)的核心成員。
前言
在享受Spring MVC帶給你便捷的時(shí)候,你是否曾經(jīng)這樣疑問過:Controller的handler方法參數(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)行描述:
- 基于
Name - 數(shù)據(jù)類型是
Map的 - 固定參數(shù)類型
- 基于
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ì)此部分的處理步驟,我把它簡述如下:
- 基于
MethodParameter構(gòu)建NameValueInfo<-- 主要有name, defaultValue, required(其實(shí)主要是解析方法參數(shù)上標(biāo)注的注解~) - 通過
BeanExpressionResolver(${}占位符以及SpEL) 解析name - 通過模版方法
resolveName從HttpServletRequest, Http Headers, URI template variables等等中獲取對(duì)應(yīng)的屬性值(具體由子類去實(shí)現(xiàn)) - 對(duì)
arg==null這種情況的處理, 要么使用默認(rèn)值, 若required = true && arg == null, 則一般報(bào)出異常(boolean類型除外~) - 通過
WebDataBinder將arg轉(zhuǎn)換成Methodparameter.getParameterType()類型(注意:這里僅僅只是用了數(shù)據(jù)轉(zhuǎn)換而已,并沒有用bind()方法)
該抽象類繼承樹如下:

從上源碼可以看出,抽象類已經(jīng)定死了處理模版(方法為final的),留給子類需要做的事就不多了,大體還有如下三件事:
- 根據(jù)
MethodParameter創(chuàng)建NameValueInfo(子類的實(shí)現(xiàn)可繼承自NameValueInfo,就是對(duì)應(yīng)注解的屬性們) - 根據(jù)方法參數(shù)名稱
name從HttpServletRequest, Http Headers, URI template variables等等中獲取屬性值 - 對(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;
}
...
}
可以看到ServletModelAttributeMethodProcessor和RequestParamMethodArgumentResolver一樣,也是有兜底的效果的。
在本文末,我搜集了一些自己使用過程中的一些疑惑進(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):
- 多個(gè)值只能使用
,號(hào)分隔才行(否則會(huì)被當(dāng)作一個(gè)值,放進(jìn)數(shù)組/集合里,不會(huì)報(bào)錯(cuò)) -
@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)你入群一起飛==