深刻分析 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>

相关文章
相关标签/搜索