天天看點

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

每篇一句

你的工作效率高,老闆會認為你強度不夠。你代碼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;
}
           

基于這個接口的處理器實作類不可謂不豐富,非常之多。我截圖如下:

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

因為子類衆多,是以我分類進行說明。我把它分為四類進行描述:

  1. 基于

    Name

  2. 資料類型是

    Map

  3. 固定參數類型
  4. 基于

    ContentType

    的消息轉換器

第一類:基于

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;
}
           

該抽象類中定義了解析參數的主邏輯(模版邏輯),子類隻需要實作對應的抽象模版方法即可。

對此部分的處理步驟,我把它簡述如下:

  1. 基于

    MethodParameter

    建構

    NameValueInfo

    <-- 主要有

    name, defaultValue, required

    (其實主要是解析方法參數上标注的注解~)
  2. 通過

    BeanExpressionResolver

    (

    ${}

    占位符以及

    SpEL

    ) 解析

    name

  3. 通過模版方法

    resolveName

    HttpServletRequest, Http Headers, URI template variables

    等等中擷取對應的屬性值(具體由子類去實作)
  4. arg==null

    這種情況的處理, 要麼使用預設值, 若

    required = true && arg == null

    , 則一般報出異常(boolean類型除外~)
  5. 通過

    WebDataBinder

    arg

    轉換成

    Methodparameter.getParameterType()

    類型(注意:這裡僅僅隻是用了資料轉換而已,并沒有用

    bind()

    方法)

該抽象類繼承樹如下:

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

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

  1. 根據

    MethodParameter

    建立

    NameValueInfo

    (子類的實作可繼承自

    NameValueInfo

    ,就是對應注解的屬性們)
  2. 根據方法參數名稱

    name

    HttpServletRequest, Http Headers, URI template variables

    等等中擷取屬性值
  3. 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);
}
           

它的三個實作類:

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

關于此接口的使用,後面再重點介紹,此處建議自動選擇性忽略。

// @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。通過

Apache JMeter

測試:非RESTful接口的性能是RESTful接口的兩倍,接口相應時間上更是達到10倍左右(是–>300ms左右 非–>20ms左右)

針對

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

使用注意事項

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

@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的

required=false

使用較少,一般用于在用URL傳多個值時,但有些值是非必傳的時候使用。比如這樣的URL:

"/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

也是可以正常接收的)。

使用時應注意如下兩點:

  1. 多個值隻能使用

    ,

    号分隔才行(否則會被當作一個值,放進數組/集合裡,不會報錯)
  2. @PathVariable

    注解是必須的。否則會交給

    ServletModelAttributeMethodProcessor

    兜底去處理,它要求有空構造是以反射建立執行個體會報錯(數組/List)。(注意:如果是這樣寫

    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

注解是必須存在的,否則報錯如下(因為交給兜底處理了):

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

但如果你這麼寫

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
E-mail [email protected]
微 信 fsx641385712

活躍平台

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥
公衆号 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦
HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】關注A哥

繼續閱讀