每篇一句
你的工作效率高,老闆會認為你強度不夠。你代碼bug多,各種生産環境救火,老闆會覺得你是團隊的核心成員。
前言
在享受
Spring MVC
帶給你便捷的時候,你是否曾經這樣疑問過:
Controller
的
handler
方法參數能夠自動完成參數封裝(有時即使沒有
@PathVariable
、
@RequestParam
、
@RequestBody
等注解都可),甚至在方法參數任意位置寫
HttpServletRequest
、
HttpSession
、
Writer
…等類型的參數,它自動就有值了便可直接使用。
對此你是否想問一句:
Spring MVC
它是怎麼辦到的?那麼本文就揭開它的神秘面紗,還你一片"清白"。
Spring MVC
作為一個最為流行的web架構,早早已經成為了實際意義上的标準化(架構),特别是随着
Struts2
的突然崩塌,
Spring MVC
幾乎一騎絕塵,是以深入了解它有着深遠的意義
Spring MVC
它隻需要區區幾個注解就能夠讓一個普通的java方法成為一個
Handler
處理器,并且還能有自動參數封裝、傳回值視圖處理/渲染等一系列強大功能,讓coder的精力更加的聚焦在自己的業務。
像JSF、Google Web Toolkit、Grails Framework等web架構至少我是沒有用過的。
這裡有個輕量級的web架構:
設計上我個人覺得還挺有意思,有興趣的可以玩玩
Play Framework
HandlerMethodArgumentResolver
政策接口:用于在給定請求的上下文中将方法參數解析為參數值。簡單的了解為:它負責處理你
Handler
方法裡的所有入參:包括自動封裝、自動指派、校驗等等。有了它才能會讓
Spring MVC
處理入參顯得那麼進階、那麼自動化。
Spring MVC
内置了非常非常多的實作,當然若還不能滿足你的需求,你依舊可以自定義和自己注冊,後面我會給出自定義的示例。
有個形象的公式:
HandlerMethodArgumentResolver = HandlerMethod + Argument(參數) + Resolver(解析器)
。
解釋為:它是
HandlerMethod
方法的解析器,将
HttpServletRequest(header + body 中的内容)
解析為
HandlerMethod
方法的參數(method parameters)
// @since 3.1 HandlerMethod 方法中 參數解析器
public interface HandlerMethodArgumentResolver {
// 判斷 HandlerMethodArgumentResolver 是否支援 MethodParameter
// (PS: 一般都是通過 參數上面的注解|參數的類型)
boolean supportsParameter(MethodParameter parameter);
// 從NativeWebRequest中擷取資料,ModelAndViewContainer用來提供通路Model
// MethodParameter parameter:請求參數
// WebDataBinderFactory用于建立一個WebDataBinder用于資料綁定、校驗
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
基于這個接口的處理器實作類不可謂不豐富,非常之多。我截圖如下:
因為子類衆多,是以我分類進行說明。我把它分為四類進行描述:
- 基于
Name
- 資料類型是
的Map
- 固定參數類型
- 基于
的消息轉換器ContentType
第一類:基于 Name
Name
從URI(路徑變量)、HttpServletRequest、HttpSession、Header、Cookie…等中根據名稱key來擷取值
這類處理器所有的都是基于抽象類
AbstractNamedValueMethodArgumentResolver
來實作,它是最為重要的分支(分類)。
// @since 3.1 負責從路徑變量、請求、頭等中拿到值。(都可以指定name、required、預設值等屬性)
// 子類需要做如下事:擷取方法參數的命名值資訊、将名稱解析為參數值
// 當需要參數值時處理缺少的參數值、可選地處了解析值
//特别注意的是:預設值可以使用${}占位符,或者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;
// 預設是RequestScope
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);
}
// protected的内部類 是以所有子類(注解)都是用友這三個屬性值的
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 {
// 建立 MethodParameter 對應的 NamedValueInfo
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
// 支援到了Java 8 中支援的 java.util.Optional
MethodParameter nestedParameter = parameter.nestedIfOptional();
// name屬性(也就是注解标注的value/name屬性)這裡既會解析占位符,還會解析SpEL表達式,非常強大
// 因為此時的 name 可能還是被 ${} 符号包裹, 則通過 BeanExpressionResolver 來進行解析
Object resolvedName = resolveStringValue(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 模版抽象方法:将給定的參數類型和值名稱解析為參數值。 由子類去實作
// @PathVariable --> 通過對uri解析後得到的decodedUriVariables值(常用)
// @RequestParam --> 通過 HttpServletRequest.getParameterValues(name) 擷取(常用)
// @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) {
// 可以發現:defaultValue也是支援占位符和SpEL的~~~
if (namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
// 若 arg == null && defaultValue == null && 非 optional 類型的參數 則通過 handleMissingValue 來進行處理, 一般是報異常
} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
// 它是個protected方法,預設抛出ServletRequestBindingException異常
// 各子類都複寫了此方法,轉而抛出自己的異常(但都是ServletRequestBindingException的異常子類)
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
// handleNullValue是private方法,來處理null值
// 針對Bool類型有這個判斷:Boolean.TYPE.equals(paramType) 就return Boolean.FALSE;
// 此處注意:Boolean.TYPE = Class.getPrimitiveClass("boolean") 它指的基本類型的boolean,而不是Boolean類型哦~~~
// 如果到了這一步(value是null),但你還是基本類型,那就抛出異常了(隻有boolean類型不會抛異常哦~)
// 這裡多嘴一句,即使請求傳值為&bool=1,效果同bool=true的(1:true 0:false) 并且不區分大小寫哦(TrUe效果同true)
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
// 相容空串,若傳入的是空串,依舊還是使用預設值(預設值支援占位符和SpEL)
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
// 完成自動化的資料綁定~~~
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
// 通過資料綁定器裡的Converter轉換器把arg轉換為指定類型的數值
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException ex) { // 注意這個異常: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的方法,本類為空實作,交給子類去複寫(并不是必須的)
// 唯獨隻有PathVariableMethodArgumentResolver把解析處理啊的值存儲一下資料到
// HttpServletRequest.setAttribute中(若key已經存在也不會存儲了)
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
// 此處有緩存,記錄下每一個MethodParameter對象 value是NamedValueInfo值
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
if (namedValueInfo == null) {
// createNamedValueInfo是抽象方法,子類必須實作
namedValueInfo = createNamedValueInfo(parameter);
// updateNamedValueInfo:這一步就是我們之前說過的為何Spring MVC可以根據參數名封裝的方法
// 如果info.name.isEmpty()的話(注解裡沒指定名稱),就通過`parameter.getParameterName()`去擷取參數名~
// 它還會處理注解指定的defaultValue:`\n\t\.....`等等都會被當作null處理
// 都處理好後:new NamedValueInfo(name, info.required, defaultValue);(相當于吧注解解析成了此對象嘛~~)
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
this.namedValueInfoCache.put(parameter, namedValueInfo);
}
return namedValueInfo;
}
// 抽象方法
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
// 由子類根據名稱,去把值拿出來
protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception;
}
該抽象類中定義了解析參數的主邏輯(模版邏輯),子類隻需要實作對應的抽象模版方法即可。
對此部分的處理步驟,我把它簡述如下:
- 基于
建構MethodParameter
<-- 主要有NameValueInfo
(其實主要是解析方法參數上标注的注解~)name, defaultValue, required
- 通過
(BeanExpressionResolver
占位符以及${}
) 解析SpEL
name
- 通過模版方法
從resolveName
等等中擷取對應的屬性值(具體由子類去實作)HttpServletRequest, Http Headers, URI template variables
- 對
這種情況的處理, 要麼使用預設值, 若arg==null
, 則一般報出異常(boolean類型除外~)required = true && arg == null
- 通過
将WebDataBinder
轉換成arg
類型(注意:這裡僅僅隻是用了資料轉換而已,并沒有用Methodparameter.getParameterType()
方法)bind()
該抽象類繼承樹如下:
從上源碼可以看出,抽象類已經定死了處理模版(方法為final的),留給子類需要做的事就不多了,大體還有如下三件事:
- 根據
建立MethodParameter
(子類的實作可繼承自NameValueInfo
,就是對應注解的屬性們)NameValueInfo
- 根據方法參數名稱
從name
等等中擷取屬性值HttpServletRequest, Http Headers, URI template variables
- 對
這種情況的處理(非必須)arg == null
PathVariableMethodArgumentResolver
它幫助
Spring MVC
實作restful風格的URL。它用于處理标注有
@PathVariable
注解的方法參數,用于從URL中擷取值(并不是?後面的參數哦)。
并且,并且,并且它還可以解析
@PathVariable
注解的value值不為空的Map(使用較少,個人不太建議使用)~
UriComponentsContributor
接口:通過檢視方法參數和參數值并決定應更新目标URL的哪個部分,為建構
UriComponents
的政策接口。
// @since 4.0 出現得還是比較晚的
public interface UriComponentsContributor {
// 此方法完全同HandlerMethodArgumentResolver的這個方法~~~
boolean supportsParameter(MethodParameter parameter);
// 處理給定的方法參數,然後更新UriComponentsbuilder,或者使用uri變量添加到映射中,以便在處理完所有參數後用于擴充uri~~~
void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder,
Map<String, Object> uriVariables, ConversionService conversionService);
}
它的三個實作類:
關于此接口的使用,後面再重點介紹,此處建議自動選擇性忽略。
// @since 3.0 需要注意的是:它隻支援标注在@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 它也是标記為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是必須,不管你啥類型
// 标注了注解,且是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) {
// 預設值使用的DEFAULT_NONE~~~
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
}
}
// 根據name去拿值的過程非常之簡單,但是它和前面的隻知識是有關聯的
// 至于這個attr是什麼時候放進去的,AbstractHandlerMethodMapping.handleMatch()比對處理器方法上
// 通過UrlPathHelper.decodePathVariables() 把參數提取出來了,然後放進request屬性上暫存了~~~
// 關于HandlerMapping内容,可來這裡: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);
}
// 值完全處理結束後,把處理好的值放進請求域,友善view裡渲染時候使用~
// 抽象父類的handleResolvedValue方法,隻有它複寫了~
@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);
}
...
}
關于
@PathVariable
的使用,不用再給例子了。
說明:因為使用路徑參數需要進行複雜的比對流程以及正則比對,所有效率相較來說低些,若以若是那種對響應事件強要求的(比如記錄點選事件…),建議用請求參數代替(當然你也可以重寫
RequestMappingHandlerMapping
的URL比對方法來定制化你的需求)。
GET /list/cityId/1 屬于RESTful /list/cityId?cityId=1不屬于RESTful。通過
測試:非RESTful接口的性能是RESTful接口的兩倍,接口相應時間上更是達到10倍左右(是–>300ms左右 非–>20ms左右)
Apache JMeter
針對
RESTful
此處我提出一個思考題:若你是一個現成的系統,現對相應提出要求:接口耗時必須控制在50ms以内,怎麼破?
思路一:将所有的url修改為非RESTful風格(不使用
@PathVariable
)
痛點:系統已存在幾百個接口,若修改不僅需要修改服務端,用戶端也得改,工作量太大。并且稍有不慎,容易造成404現象~
思路二:定制化
AbstractHandlerMethodMapping#lookupHandlerMethod
方法
此方法負責URL的比對,我們為了提效其實就是為了避免一些正則比對(
AntPathMatcher
)。
對此文答案有興趣的可參見此文:SpringMVC RESTful 性能優化
唯一需要說一下如果類型是
Map
類型的情況下的使用注意事項,如下:
@PathVariable("jsonStr") Map<String,Object> map
希望把
jsonStr
對應的字元串解析成鍵值對封裝進
Map
裡。那麼你必須,必須,必須注冊了能處理此字元串的
Converter/PropertyEditor
(自定義)。使用起來相對麻煩,但技術隐蔽性高。我一般不建議這麼來用~
關于@PathVariable的 required=false
使用注意事項
required=false
這個功能是很多人比較疑問的,如何使用???
@ResponseBody
@GetMapping("/test/{id}")
public Person test(@PathVariable(required = false) Integer id) { ... }
以為這樣寫通過
/test
這個url就能通路到了,其實這樣是不行的,會404。
正确姿勢:
@ResponseBody
@GetMapping({"/test/{id}", "/test"})
public Person test(@PathVariable(required = false) Integer id) { ... }
這樣
/test
和
/test/1
這兩個url就都能正常work了~
@PathVariable的使用較少,一般用于在用URL傳多個值時,但有些值是非必傳的時候使用。比如這樣的URL:
required=false
"/user/{id}/{name}","/user/{id}","/user"
RequestParamMethodArgumentResolver
顧名思義,是解析标注有
@RequestParam
的方法入參解析器,這個注解比上面的注解強大很多了,它用于從請求參數(?後面的)中擷取值完成封裝。這是我們的絕大多數使用場景。除此之外,它還支援
MultipartFile
,也就是說能夠從
MultipartHttpServletRequest | HttpServletRequest
擷取資料,并且并且并且還兜底處理沒有标注任何注解的“簡單類型”~
// @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);
// 這個參數老重要了:
// true:表示參數類型是基本類型 參考BeanUtils#isSimpleProperty(什麼Enum、Number、Date、URL、包裝類型、以上類型的數組類型等等)
// 如果是基本類型,即使你不寫@RequestParam注解,它也是會走進來處理的~~~(這個@PathVariable可不會喲~)
// fasle:除上以外的。 要想它處理就必須标注注解才行哦,比如List等~
// 預設值是false
private final boolean useDefaultResolution;
// 此構造隻有`MvcUriComponentsBuilder`調用了 傳入的false
public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
this.useDefaultResolution = useDefaultResolution;
}
// 傳入了ConfigurableBeanFactory ,是以它支援處理占位符${...} 并且支援SpEL了
// 此構造都在RequestMappingHandlerAdapter裡調用,最後都會傳入true來Catch-all Case 這種設計挺有意思的
public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
super(beanFactory);
this.useDefaultResolution = useDefaultResolution;
}
// 此處理器能處理如下Case:
// 1、所有标注有@RequestParam注解的類型(非Map)/ 注解指定了value值的Map類型(自己提供轉換器哦)
// ======下面都表示沒有标注@RequestParam注解了的=======
// 1、不能标注有@RequestPart注解,否則直接不處理了
// 2、是上傳的request:isMultipartArgument() = true(MultipartFile類型或者對應的集合/數組類型 或者javax.servlet.http.Part對應結合/數組類型)
// 3、useDefaultResolution=true情況下,"基本類型"也會處理
@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注解,也是可以建立出一個NamedValueInfo來的
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}
// 内部類
private static class RequestParamNamedValueInfo extends NamedValueInfo {
// 請注意這個預設值:如果你不寫@RequestParam,那麼就會用這個預設值
// 注意:required = false的喲(若寫了注解,required預設可是true,請務必注意區分)
// 因為不寫注解的情況下,若是簡單類型參數都是交給此處理器處理的。是以這個機制需要明白
// 複雜類型(非簡單類型)預設是ModelAttributeMethodProcessor處理的
public RequestParamNamedValueInfo() {
super("", false, ValueConstants.DEFAULT_NONE);
}
public RequestParamNamedValueInfo(RequestParam annotation) {
super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
// 核心方法:根據Name 擷取值(普通/檔案上傳)
// 并且還有集合、數組等情況
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
// 這塊解析出來的是個MultipartFile或者其集合/數組
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,那處理完檔案上傳裡木有,那就去參數裡取吧
// 由此可見:檔案上傳的優先級是高于請求參數的
if (arg == null) {
//小知識點:getParameter()其實本質是getParameterNames()[0]的效果
// 強調一遍:?ids=1,2,3 結果是["1,2,3"](相容方式,不建議使用。注意:隻能是逗号分隔)
// ?ids=1&ids=2&ids=3 結果是[1,2,3](标準的傳值方式,建議使用)
// 但是Spring MVC這兩種都能用List接收 請務必注意他們的差別~~~
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
...
}
可以看到這個
ArgumentResolver
處理器還是很強大的:不僅能處理标注了
@RequestParam
的參數,還能接收檔案上傳參數。甚至那些你平時使用中不标注該注解的封裝也是它來兜底完成的。至于它如何兜底的,可以參見下面這個騷操作:
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
一樣,也是有兜底的效果的。
在本文末,我搜集了一些自己使用過程中的一些疑惑進行解惑,希望也一樣能幫助你豁然開朗。
get請求如何傳值數組、集合(List)
如題的這個
case
太常見了有木有,我們經常會遇到使用get請求向後端需要傳值的需求(比如根據ids批量查詢)。但到底如何傳,URL怎麼寫,應該是有傻傻分不清楚的不确定的情況。
@PathVariable傳參
@ResponseBody
@GetMapping("/test/{objects}")
public Object test(@PathVariable List<Object> objects) {
System.out.println(objects);
return objects;
}
請求URL:
/test/fsx,fsx,fsx
。控制台列印:
集合接收成功(使用
@PathVariable Object[] objects
也是可以正常接收的)。
使用時應注意如下兩點:
- 多個值隻能使用
号分隔才行(否則會被當作一個值,放進數組/集合裡,不會報錯),
-
注解是必須的。否則會交給@PathVariable
兜底去處理,它要求有空構造是以反射建立執行個體會報錯(數組/List)。(注意:如果是這樣寫ServletModelAttributeMethodProcessor
,那是不會報錯的,隻是值肯定是封裝不進來的,一個空對象而已)ArrayList<Object> objects
說明:為何逗号分隔的String類型預設就能轉化為數組,集合。請參考這種内置的
StringToCollectionConverter/StringToArrayConverter
通用轉換器~~
GenericConverter
@RequestParam傳參
@ResponseBody
@GetMapping("/test")
public Object test(@RequestParam List<Object> objects) {
System.out.println(objects);
return objects;
}
請求URL:
/test/?objects=1,2,3
。控制台列印:
請求URL改為:
/test/?objects=1&objects=2&objects=3
。控制台列印:
兩個請求的URL不一樣,但都能正确的達到效果。(
@RequestParam Object[] objects
這麼寫兩種URL也能正常封裝)
對此有如下這個細節你必須得注意:對于集合
List
而言
@RequestParam
注解是必須存在的,否則報錯如下(因為交給兜底處理了):
但如果你這麼寫
String[] objects
,即使不寫注解,也能夠正常完成正确封裝。
說明: Object[] objects
這麼寫的話不寫注解是不行的(報錯如上)。至于原因,各位小夥伴可以自行思考,沒想明白的話可以給我留言(建議小夥伴一定要弄明白緣由)~
PS:需要注意的是,
Spring MVC
的這麼多
HandlerMethodArgumentResolver
它的解析是有順序的:如果多個
HandlerMethodArgumentResolver
都可以解析某一種類型,以順序在前面的先解析(後面的就不會再執行解析了)。
源碼參考處: HandlerMethodArgumentResolverComposite.getArgumentResolver(MethodParameter parameter);
由于
RequestParamMethodArgumentResolver
同樣可以對
Multipart
檔案上傳進行解析,并且預設順序在
RequestPartMethodArgumentResolver
之前,是以如果不添加
@RequestPart
注解,
Multipart
類型的參數會被
RequestParamMethodArgumentResolver
解析。
總結
本文是你了解
Spring MVC
強大的自動資料封裝功能非常重要的一篇文章。它介紹了
HandlerMethodArgumentResolver
的功能和基本使用,以及深入介紹了最為重要的兩個注解
@PathVariable
和
@RequestParam
以及各自對應的
ArgumentResolver
處理器。
由于這個體系龐大,是以我會分多個章節進行描述,歡迎訂閱和持續關注~
相關閱讀
【小家Spring】Spring MVC容器的web九大元件之—HandlerMapping源碼詳解(二)—RequestMappingHandlerMapping系列
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】
HandlerMethodArgumentResolver(三):基于HttpMessageConverter消息轉換器的參數處理器【享學Spring MVC】
關注A哥
Author | A哥(YourBatman) |
---|---|
個人站點 | www.yourbatman.cn |
[email protected] | |
微 信 | fsx641385712 |
| |
公衆号 | BAT的烏托邦(ID:BAT-utopia) |
知識星球 | BAT的烏托邦 |
每日文章推薦 | 每日文章推薦 |