深刻分析 SpringMVC 參數解析器

前面和你們聊了自定義 SpringMVC 參數解析器,同時咱們也分析了幾個比較簡單的參數解析器,相信你們對於 SpringMVC 中的參數解析器應該已經有了必定的瞭解,若是還沒看過的小夥伴能夠先看看:SpringBoot 中如何自定義參數解析器?java

不過我相信不少小夥伴真正疑惑的是像下面這種接口,參數是怎麼解析的:web

@GetMapping("/hello2")
public void hello2(String name) {
    System.out.println("name = " + name);
}

抑或者像下面這種接口,參數是怎麼解析的:緩存

@GetMapping("/hello/{id}")
public void hello3(@PathVariable Long id) {
    System.out.println("id = " + id);
}

這是咱們平常中最多見的參數定義方式,相信不少小夥伴對此很感興趣。因爲這塊涉及到一個很是龐大的類 AbstractNamedValueMethodArgumentResolver,所以這裏我單獨寫了一篇文章來和你們分享這個問題。app

在正式分享以前,咱們先來總體看看參數解析器都有哪些。ide

1.參數解析器

HandlerMethodArgumentResolver 就是咱們口口聲聲說的參數解析器,它的實現類仍是蠻多的,由於每一種類型的參數都對應了一個參數解析器:工具

爲了理解方便,咱們能夠將這些參數解析器分爲四大類:ui

  • xxxMethodArgumentResolver:這就是一個普通的參數解析器。
  • xxxMethodProcessor:不只能夠看成參數解析器,還能夠處理對應類型的返回值。
  • xxxAdapter:這種不作參數解析,僅僅用來做爲 WebArgumentResolver 類型的參數解析器的適配器。
  • HandlerMethodArgumentResolverComposite:這個看名字就知道是一個組合解析器,它是一個代理,具體代理其餘幹活的那些參數解析器。

大體上能夠分爲這四類,其中最重要的固然就是前兩種了。this

2.參數解析器概覽

接下來咱們來先來大概看看這些參數解析器分別都是用來幹什麼的。url

MapMethodProcessor.net

這個用來處理 Map/ModelMap 類型的參數,解析完成後返回 model。

PathVariableMethodArgumentResolver

這個用來處理使用了 @PathVariable 註解而且參數類型不爲 Map 的參數,參數類型爲 Map 則使用 PathVariableMapMethodArgumentResolver 來處理。

PathVariableMapMethodArgumentResolver

見上。

ErrorsMethodArgumentResolver

這個用來處理 Error 參數,例如咱們作參數校驗時的 BindingResult。

AbstractNamedValueMethodArgumentResolver

這個用來處理 key/value 類型的參數,如請求頭參數、使用了 @PathVariable 註解的參數以及 Cookie 等。

RequestHeaderMethodArgumentResolver

這個用來處理使用了 @RequestHeader 註解,而且參數類型不是 Map 的參數(參數類型是 Map 的使用 RequestHeaderMapMethodArgumentResolver)。

RequestHeaderMapMethodArgumentResolver

見上。

RequestAttributeMethodArgumentResolver

這個用來處理使用了 @RequestAttribute 註解的參數。

RequestParamMethodArgumentResolver

這個功能就比較廣了。使用了 @RequestParam 註解的參數、文件上傳的類型 MultipartFile、或者一些沒有使用任何註解的基本類型(Long、Integer)以及 String 等,都使用該參數解析器處理。須要注意的是,若是 @RequestParam 註解的參數類型是 Map,則該註解必須有 name 值,不然解析將由 RequestParamMapMethodArgumentResolver 完成。

RequestParamMapMethodArgumentResolver

見上。

AbstractCookieValueMethodArgumentResolver

這個是一個父類,處理使用了 @CookieValue 註解的參數。

ServletCookieValueMethodArgumentResolver

這個處理使用了 @CookieValue 註解的參數。

MatrixVariableMethodArgumentResolver

這個處理使用了 @MatrixVariable 註解而且參數類型不是 Map 的參數,若是參數類型是 Map,則使用 MatrixVariableMapMethodArgumentResolver 來處理。

MatrixVariableMapMethodArgumentResolver

見上。

SessionAttributeMethodArgumentResolver

這個用來處理使用了 @SessionAttribute 註解的參數。

ExpressionValueMethodArgumentResolver

這個用來處理使用了 @Value 註解的參數。

ServletResponseMethodArgumentResolver

這個用來處理 ServletResponse、OutputStream 以及 Writer 類型的參數。

ModelMethodProcessor

這個用來處理 Model 類型參數,並返回 model。

ModelAttributeMethodProcessor

這個用來處理使用了 @ModelAttribute 註解的參數。

SessionStatusMethodArgumentResolver

這個用來處理 SessionStatus 類型的參數。

PrincipalMethodArgumentResolver

這個用來處理 Principal 類型參數,這個鬆哥在前面的文章中和你們介紹過了(SpringBoot 中如何自定義參數解析器?)。

AbstractMessageConverterMethodArgumentResolver

這是一個父類,當使用 HttpMessageConverter 解析 requestbody 類型參數時,相關的處理類都會繼承自它。

RequestPartMethodArgumentResolver

這個用來處理使用了 @RequestPart 註解、MultipartFile 以及 Part 類型的參數。

AbstractMessageConverterMethodProcessor

這是一個工具類,不承擔參數解析任務。

RequestResponseBodyMethodProcessor

這個用來處理添加了 @RequestBody 註解的參數。

HttpEntityMethodProcessor

這個用來處理 HttpEntity 和 RequestEntity 類型的參數。

ContinuationHandlerMethodArgumentResolver

AbstractWebArgumentResolverAdapter

這種不作參數解析,僅僅用來做爲 WebArgumentResolver 類型的參數解析器的適配器。

ServletWebArgumentResolverAdapter

這個給父類提供 request。

UriComponentsBuilderMethodArgumentResolver

這個用來處理 UriComponentsBuilder 類型的參數。

ServletRequestMethodArgumentResolver

這個用來處理 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 類型的參數。

HandlerMethodArgumentResolverComposite

這個看名字就知道是一個組合解析器,它是一個代理,具體代理其餘幹活的那些參數解析器。

RedirectAttributesMethodArgumentResolver

這個用來處理 RedirectAttributes 類型的參數,RedirectAttributes 鬆哥在以前的文章中和你們介紹過:SpringMVC 中的參數還能這麼傳遞?漲姿式了!

好了,各個參數解析器的大體功能就給你們介紹完了,接下來咱們選擇其中一種,來具體說說它的源碼。

3.AbstractNamedValueMethodArgumentResolver

AbstractNamedValueMethodArgumentResolver 是一個抽象類,一些鍵值對類型的參數解析器都是經過繼承它實現的,它裏邊定義了不少這些鍵值對類型參數解析器的公共操做。

AbstractNamedValueMethodArgumentResolver 中也是應用了不少模版模式,例如它沒有實現 supportsParameter 方法,該方法的具體實如今不一樣的子類中,resolveArgument 方法它卻是實現了,咱們一塊兒來看下:

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
	NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
	MethodParameter nestedParameter = parameter.nestedIfOptional();
	Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
	if (resolvedName == null) {
		throw new IllegalArgumentException(
				"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
	}
	Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
	if (arg == null) {
		if (namedValueInfo.defaultValue != null) {
			arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
		}
		else if (namedValueInfo.required && !nestedParameter.isOptional()) {
			handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
		}
		arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
	}
	else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
		arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
	}
	if (binderFactory != null) {
		WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
		try {
			arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
		}
		catch (ConversionNotSupportedException ex) {
			throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
					namedValueInfo.name, parameter, ex.getCause());
		}
		catch (TypeMismatchException ex) {
			throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
					namedValueInfo.name, parameter, ex.getCause());
		}
		// Check for null value after conversion of incoming argument value
		if (arg == null && namedValueInfo.defaultValue == null &&
				namedValueInfo.required && !nestedParameter.isOptional()) {
			handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
		}
	}
	handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
	return arg;
}
  1. 首先根據當前請求獲取一個 NamedValueInfo 對象,這個對象中保存了參數的三個屬性:參數名、參數是否必須以及參數默認值。具體的獲取過程就是先去緩存中拿,緩存中若是有,就直接返回,緩存中若是沒有,則調用 createNamedValueInfo 方法去建立,將建立結果緩存起來並返回。createNamedValueInfo 方法是一個模版方法,具體的實如今子類中。
  2. 接下來處理 Optional 類型參數。
  3. resolveEmbeddedValuesAndExpressions 方法是爲了處理註解中使用了 SpEL 表達式的狀況,例如以下接口:
@GetMapping("/hello2")
public void hello2(@RequestParam(value = "${aa.bb}") String name) {
    System.out.println("name = " + name);
}

參數名使用了表達式,那麼 resolveEmbeddedValuesAndExpressions 方法的目的就是解析出表達式的值,若是沒用到表達式,那麼該方法會將原參數原封不動返回。 4. 接下來調用 resolveName 方法解析出參數的具體值,這個方法也是一個模版方法,具體的實如今子類中。 5. 若是獲取到的參數值爲 null,先去看註解中有沒有默認值,而後再去看參數值是不是必須的,若是是,則拋異常出來,不然就設置爲 null 便可。 6. 若是解析出來的參數值爲空字符串 "",則也去 resolveEmbeddedValuesAndExpressions 方法中走一遭。 7. 最後則是 WebDataBinder 的處理,解決一些全局參數的問題,WebDataBinder 鬆哥在以前的文章中也有介紹過,傳送門:@ControllerAdvice 的三種使用場景

大體的流程就是這樣。

在這個流程中,咱們看到主要有以下兩個方法是在子類中實現的:

  • createNamedValueInfo
  • resolveName

在加上 supportsParameter 方法,子類中一共有三個方法須要咱們重點分析。

那麼接下來咱們就以 RequestParamMethodArgumentResolver 爲例,來看下這三個方法。

4.RequestParamMethodArgumentResolver

4.1 supportsParameter

@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;
		}
	}
}
public static boolean isSimpleProperty(Class<!--?--> type) {
	return isSimpleValueType(type) || (type.isArray() &amp;&amp; isSimpleValueType(type.getComponentType()));
}
public static boolean isSimpleValueType(Class<!--?--> type) {
	return (Void.class != type &amp;&amp; void.class != type &amp;&amp;
			(ClassUtils.isPrimitiveOrWrapper(type) ||
			Enum.class.isAssignableFrom(type) ||
			CharSequence.class.isAssignableFrom(type) ||
			Number.class.isAssignableFrom(type) ||
			Date.class.isAssignableFrom(type) ||
			Temporal.class.isAssignableFrom(type) ||
			URI.class == type ||
			URL.class == type ||
			Locale.class == type ||
			Class.class == type));
}

從 supportsParameter 方法中能夠很是方便的看出支持的參數類型:

  1. 首先參數若是有 @RequestParam 註解的話,則分兩種狀況:參數類型若是是 Map,則 @RequestParam 註解必須配置 name 屬性,不然不支持;若是參數類型不是 Map,則直接返回 true,表示老是支持(想一想本身平時使用的時候是否是這樣)。
  2. 參數若是含有 @RequestPart 註解,則不支持。
  3. 檢查下是否是文件上傳請求,若是是,返回 true 表示支持。
  4. 若是前面都沒能返回,則使用默認的解決方案,判斷是否是簡單類型,主要就是 Void、枚舉、字符串、數字、日期等等。

這塊代碼其實很簡單,支持誰不支持誰,一目瞭然。

4.2 createNamedValueInfo

@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 {
	public RequestParamNamedValueInfo() {
		super("", false, ValueConstants.DEFAULT_NONE);
	}
	public RequestParamNamedValueInfo(RequestParam annotation) {
		super(annotation.name(), annotation.required(), annotation.defaultValue());
	}
}

獲取註解,讀取註解中的屬性,構造 RequestParamNamedValueInfo 對象返回。

4.3 resolveName

@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
	HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
	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);
		}
	}
	if (arg == null) {
		String[] paramValues = request.getParameterValues(name);
		if (paramValues != null) {
			arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
		}
	}
	return arg;
}

這個方法思路也比較清晰:

  1. 前面兩個 if 主要是爲了處理文件上傳請求。
  2. 若是不是文件上傳請求,則調用 request.getParameterValues 方法取出參數返回便可。

整個過程仍是比較 easy 的。小夥伴們能夠在此基礎之上自行分析 PathVariableMethodArgumentResolver 的原理,也很容易。

5.小結

今天主要和小夥伴們梳理了 SpringMVC 參數解析器的整個體系,關於這些解析器在什麼時候被配置,在什麼時候被調用,鬆哥在後面的文章中會和你們繼續分析。好啦,今天就說這麼多。</multipartfile>

相關文章
相關標籤/搜索